自定义ViewpagerIndicator (仿猫眼,添加边缘回弹滚动效果)
2016-03-25 16:14
627 查看
一.概述
今天主要来分享个自定义viewpagerindicator,效果主要是仿 猫眼电影 顶部的栏目切换,也就是我们常说的indicator,难度简单,为了让滑动时效果更炫酷,我在滑动到左边第一个item或者最右边的item时,添加了滑动到边缘位置后,回弹然后复位的效果(其实也是很简单,只要计算好距离就好啦)大致的效果图就是这样。大家可以凑合看看(可以看到当滑动到边缘位置的时候有回弹的效果,是不是挺带感的O(∩_∩)O)
二.使用方法
layout布局<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:background="@color/red"> <mr_immortalz.com.viewpagerindicator.ViewPagerIndicator android:id="@+id/indicator" android:layout_width="200dp" android:layout_height="36dp"></mr_immortalz.com.viewpagerindicator.ViewPagerIndicator> </LinearLayout> <android.support.v4.view.ViewPager android:id="@+id/vp" android:layout_width="match_parent" android:layout_height="match_parent"></android.support.v4.view.ViewPager> </LinearLayout>
2.MainActivity使用方法
public class MainActivity extends AppCompatActivity { private ViewPager viewPager; private ViewPagerIndicator indicator; private FragmentPagerAdapter mAdapter; private List<Fragment> mList; private List<String> mDatas; private int itemCount = 2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewPager = (ViewPager) findViewById(R.id.vp); indicator = (ViewPagerIndicator) findViewById(R.id.indicator); mList = new ArrayList<Fragment>(); for (int i = 0; i < itemCount; i++) { Fragment fragment = new MeFragment(); mList.add(fragment); } mDatas = new ArrayList<>(); for (int i = 0; i < itemCount; i++) { mDatas.add("i=" + i); } mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public Fragment getItem(int position) { return mList.get(position); } @Override public int getCount() { return mList.size(); } }; viewPager.setAdapter(mAdapter); //将viewpager与indicator绑定 indicator.setDatas(mDatas); indicator.setViewPager(viewPager); } }
3.自定义ViewpagerIndicator
public class ViewPagerIndicator extends LinearLayout { private ViewPager mViewPager; private int width; private int height; private int visibleItemCount = 3; private int itemCount = 3; //绘制框框 private Paint paint; private float mWidth = 0; private float mHeight = 0; private float mLeft = 0; private float mTop = 0; private float radiusX = 10; private float radiusY = 10; private int mPadding = 8; private List<String> mDatas; private boolean isSetData = false; private Context context; private int currentPosition; private boolean isAutoSelect = false;//判断是否进行切换 private float rebounceOffset; public ViewPagerIndicator(Context context) { super(context); this.context = context; init(); } public ViewPagerIndicator(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; init(); } public ViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; init(); } private void init() { this.setBackgroundDrawable(getResources().getDrawable(R.drawable.bg)); paint = new Paint(); paint.setStyle(Paint.Style.FILL); paint.setColor(getResources().getColor(R.color.white)); paint.setAntiAlias(true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); width = getMeasuredWidth(); height = getMeasuredHeight(); mWidth = width / visibleItemCount; mHeight = height; LogUtil.m("width " + width + " height " + height); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { LogUtil.m(); super.onSizeChanged(w, h, oldw, oldh); if (isSetData) { isSetData = false; this.removeAllViews(); //添加TextView for (int i = 0; i < mDatas.size(); i++) { TextView tv = new TextView(context); tv.setPadding(mPadding, mPadding, mPadding, mPadding); tv.setText(mDatas.get(i)); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); lp.width = width / visibleItemCount; lp.height = height; tv.setGravity(Gravity.CENTER); tv.setTextColor(getResources().getColor(R.color.font_red)); tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); tv.setLayoutParams(lp); final int finalI = i; tv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mViewPager != null) { mViewPager.setCurrentItem(finalI); } } }); this.addView(tv); } setTitleColor(); } } @Override protected void onFinishInflate() { super.onFinishInflate(); } @Override protected void onDraw(Canvas canvas) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //drawRoundRect需要的最低API是21 canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint); } else { canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint); //canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint); } } @Override protected void dispatchDraw(Canvas canvas) { //ogUtil.m(); super.dispatchDraw(canvas); } public void setViewPager(ViewPager viewpager, int position) { this.mViewPager = viewpager; this.currentPosition = position; if (mViewPager != null) { viewpager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { //当移动的是最左边item if (isAutoSelect && currentPosition == 0) { //滑动手松开时,让最左边(即第一个)item滑动到左边缘位置 if (positionOffset > rebounceOffset / 2) { mLeft = (position + (positionOffset - rebounceOffset / 2) * 2) * mWidth; } else if (positionOffset > rebounceOffset / 3 && positionOffset < rebounceOffset / 2) { //让最左边(即第一个)item 向右回弹一部分距离 mLeft = (position + (rebounceOffset / 2) - positionOffset) * mWidth * 6 / 12; } else { //让最左边(即最后一个)item 向左回弹到边缘位置 mLeft = (position + positionOffset) * mWidth * 6 / 12; } invalidate(); } else if (isAutoSelect && currentPosition == itemCount - 1) { //当移动的是最右边(即最后一个)item //滑动手松开时,让最右边(即最后一个)item滑动到右边缘位置 if (positionOffset >= rebounceOffset && positionOffset < (1 - (1 - rebounceOffset) / 2)) { // mLeft = (position + positionOffset / (1 - (1 - rebounceOffset) / 2)) * mWidth; //当item数大于visibleItem可见数,本控件(本质LinearLayout)才滚动 if (visibleItemCount < itemCount) { scrollTo((int) (mWidth * positionOffset / (1 - (1 - rebounceOffset) / 2) + (position - visibleItemCount + 1) * mWidth), 0); } if ((mLeft + mWidth) > (getChildCount() * mWidth)) { //当(mLeft + mWidth)大于最边缘的宽度时,设置 mLeft = (itemCount - 1) * mWidth; } } else if (positionOffset > (1 - (1 - rebounceOffset) / 2) && positionOffset < (1 - (1 - rebounceOffset) / 4)) { //让最右边(即最后一个)item 向左回弹一部分距离 //当item数大于visibleItem可见数,且本控件未滚动到指定位置,则设置控件滚动到指定位置 if (visibleItemCount < itemCount && getScrollX() != (itemCount - visibleItemCount) * mWidth) { scrollTo((int) ((itemCount - visibleItemCount) * mWidth), 0); } mLeft = (position + 1) * mWidth - (positionOffset - (1 - (1 - rebounceOffset) / 2)) * mWidth * 7 / 12; } else { //让最右边(即最后一个)item 向右回弹到边缘位置 //因为onPageScrolled 最后positionOffset会变成0,所以这里需要判断一下 //当positionOffset = 0 时,设置mLeft位置 if (positionOffset != 0) { mLeft = (position + 1) * mWidth - (1.0f - positionOffset) * mWidth * 7 / 12; if (mLeft > (itemCount - 1) * mWidth) { mLeft = (itemCount - 1) * mWidth; } } else { mLeft = (itemCount - 1) * mWidth; } } invalidate(); } else { //当移动的是中间item scrollTo(position, positionOffset); rebounceOffset = positionOffset; } setTitleColor(); } @Override public void onPageSelected(int position) { LogUtil.m("position " + position); currentPosition = position; } @Override public void onPageScrollStateChanged(int state) { LogUtil.m("state " + state); if (state == 2) { //当state = 2时,表示手松开,viewpager自动滑动 isAutoSelect = true; } if (state == 0) { //当state = 0时,表示viewpager滑动停止 isAutoSelect = false; } } }); } } public void setViewPager(ViewPager viewpager) { setViewPager(viewpager, 0); } /** * 正常滑动 * @param position * @param positionOffset */ private void scrollTo(int position, float positionOffset) { if (visibleItemCount < itemCount) { if (positionOffset > 0 && position > (visibleItemCount - 2)) { this.scrollTo((int) (mWidth * positionOffset + (position - visibleItemCount + 1) * mWidth), 0); } } mLeft = (position + positionOffset) * mWidth; invalidate(); } /** * 设置字体颜色 */ private void setTitleColor() { if (getChildCount() > 0) { for (int i = 0; i < getChildCount(); i++) { if (i == currentPosition) { ((TextView) getChildAt(currentPosition)).setTextColor(getResources().getColor(R.color.font_red)); } else { ((TextView) getChildAt(i)).setTextColor(getResources().getColor(R.color.font_white)); } } } } /** * 设置内容数据 * * @param mDatas */ public void setDatas(List<String> mDatas) { this.isSetData = true; this.mDatas = mDatas; this.itemCount = mDatas.size(); if (itemCount < visibleItemCount) { visibleItemCount = itemCount; } } }
三.代码分析
很明显,核心代码在ViewPagerIndicator中,因为代码中已经对每个函数方法给出了注释,下面说下大体思路。1.首先init(),onMeasure中对paint,width,height等必不可少的数据进行获取。
2.因为整个indicator是继承自linearlayout,对于里面的文字展示,用textview来显示,因为不知道用户使用的时候到底有多少个item,所以在setDatas()方法中对textview数目进行绑定。然后在onSizeChanged中动态生成需要的textview数目(isSetData用来控制是否绑定了数据,绑定了的话,需要将之前所有生成的全部清空)
protected void onSizeChanged(int w, int h, int oldw, int oldh) { LogUtil.m(); super.onSizeChanged(w, h, oldw, oldh); if (isSetData) { isSetData = false; this.removeAllViews(); //添加TextView for (int i = 0; i < mDatas.size(); i++) { TextView tv = new TextView(context); tv.setPadding(mPadding, mPadding, mPadding, mPadding); tv.setText(mDatas.get(i)); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); lp.width = width / visibleItemCount; lp.height = height; tv.setGravity(Gravity.CENTER); tv.setTextColor(getResources().getColor(R.color.font_red)); tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); tv.setLayoutParams(lp); final int finalI = i; tv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mViewPager != null) { mViewPager.setCurrentItem(finalI); } } }); this.addView(tv); } setTitleColor(); } }
只所以在onsizechanged中动态添加,是因为该方法会在ondraw前,onMeasure方法后回调,这样就保证我们能获取到需要的width,height。
3.Ok,现在获取到需要绘制的数目后接下来就是绘制白色背景框框啦。
protected void onDraw(Canvas canvas) { LogUtil.m(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //drawRoundRect需要的最低API是21 canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint); } else { canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint); //canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint); } }
很好理解,不解释`(∩_∩)′
4.接下来,最最关键的就是setViewPager()这个方法。
为了方便理解,大家可以看看
onPageScrolled(页面滚动时回调)
onPageSelected(滑动松手后回调,在一个滑动流程中只会回调一次)
onPageScrollStateChanged(在一个滑动流程中会回调三次,具体代表含义可以看图中标注)
这三个方法滑动时,具体回调顺序。
从第一个item向右滑动到第二个item
从第二个item滑动到第一个item(无论左滑还是右滑回调流程都一致)
知道了上面我们就应该很好理解了。
在onPageSelected中记录currentPosition的值。
在onPageScrollStateChanged中判断何时松开手,方便后面在松开手会对滑动进行处理
在onPageScrolled中进行滑动处理。
下面在详细说说onPageScrolled。
onPageScrolled中也有三个判断
1.处于最左边item且手滑动松开
2.处于最右边item且手滑动松开
3.其他item不管手是否滑动松开(这里用rebounceOffset记录手松开时,已经拖动的比例positionOffset)
else { //当移动的是中间item scrollTo(position, positionOffset); rebounceOffset = positionOffset; }
private void scrollTo(int position, float positionOffset) { //item数量大于可见item,linearlayout才滑动 if (visibleItemCount < itemCount) { if (positionOffset > 0 && position > (visibleItemCount - 2)) { this.scrollTo((int) (mWidth * positionOffset + (position - visibleItemCount + 1) * mWidth), 0); } } mLeft = (position + positionOffset) * mWidth; invalidate(); }
分析第一种情况。
为了实现回弹。在松手后的(positionOffset-0 ) 的时间段呢,分成三部分
看图
if (isAutoSelect && currentPosition == 0) { //滑动手松开时,让最左边(即第一个)item滑动到左边缘位置 if (positionOffset > rebounceOffset / 2) { mLeft = (position + (positionOffset - rebounceOffset / 2) * 2) * mWidth; } else if (positionOffset > rebounceOffset / 3 && positionOffset < rebounceOffset / 2) { //让最左边(即第一个)item 向右回弹一部分距离 mLeft = (position + (rebounceOffset / 2) - positionOffset) * mWidth * 6 / 12; } else { //让最左边(即最后一个)item 向左回弹到边缘位置 mLeft = (position + positionOffset) * mWidth * 6 / 12; } invalidate(); }
分析第二种情况(剩余时间(positionOffset - 1 )也是分成了三部分,一部分回到边缘,一部分偏移,一部分用于复位,与第一种情况相似,不再贴图),当item滑向最有边缘时,与第一种情况不同的是,Linearlayout是需要向左移动的,所以liearlayout向左移动了X,我们绘制的白色边框需要向右移动X,才能保证,视觉上看起来白色边框没有动,动的是,我们的Linearlayout(不知道大家能理解不,可能我说的有点不太好理解,用纸好好绘制下简单理解些`(∩_∩)′)
else if (isAutoSelect && currentPosition == itemCount - 1) { //当移动的是最右边(即最后一个)item //滑动手松开时,让最右边(即最后一个)item滑动到右边缘位置 if (positionOffset >= rebounceOffset && positionOffset < (1 - (1 - rebounceOffset) / 2)) { // mLeft = (position + positionOffset / (1 - (1 - rebounceOffset) / 2)) * mWidth; //当item数大于visibleItem可见数,本控件(本质LinearLayout)才滚动 if (visibleItemCount < itemCount) { scrollTo((int) (mWidth * positionOffset / (1 - (1 - rebounceOffset) / 2) + (position - visibleItemCount + 1) * mWidth), 0); } if ((mLeft + mWidth) > (getChildCount() * mWidth)) { //当(mLeft + mWidth)大于最边缘的宽度时,设置 mLeft = (itemCount - 1) * mWidth; } } else if (positionOffset > (1 - (1 - rebounceOffset) / 2) && positionOffset < (1 - (1 - rebounceOffset) / 4)) { //让最右边(即最后一个)item 向左回弹一部分距离 //当item数大于visibleItem可见数,且本控件未滚动到指定位置,则设置控件滚动到指定位置 if (visibleItemCount < itemCount && getScrollX() != (itemCount - visibleItemCount) * mWidth) { scrollTo((int) ((itemCount - visibleItemCount) * mWidth), 0); } mLeft = (position + 1) * mWidth - (positionOffset - (1 - (1 - rebounceOffset) / 2)) * mWidth * 7 / 12; }
OK,三种情况都分析完毕。最后我们的控件也算是大功告成啦`(∩_∩)′
源码下载地址 https://github.com/ImmortalZ/ViewPagerIndicator
欢迎star,fork!`(∩_∩)′
相关文章推荐
- linux学习总结?
- hibernate分页
- java 泛型小总结
- C#语音录制
- leetcode-5 Longest Palindromic Substring(回文子序列)
- CentOS6.6安装CloudStack4.8
- 【BZOJ 2844】 albus就是要第一个出场|线性基
- struts2.properties
- Spring和Mybatis整合时无法读取properties的处理方案
- C++中有关数组的一些细节问题
- android使用mpchartlib设置图表
- 一篇文章,带你明白什么是过拟合,欠拟合以及交叉验证
- 如何计算树中叶子结点的个数?
- POSIX多线程程序设计 工作流例程2
- 图片.pkm格式转换为.png格式
- capistrano3.4.0部署不能自动登录
- 当CAD图形中无法进行复制、粘贴时,请将此DWG另存一下或许就可以了
- capistrano3.4.0部署不能自动登录
- StudyJams第一课的学习
- 关于整数和真分数的四则运算的算法,整数已通过,但真分数不能化到最简的问题求大神指导啊