自定义ViewpagerIndicator (仿猫眼,添加边缘回弹滚动效果)

Posted Mr_immortalZ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义ViewpagerIndicator (仿猫眼,添加边缘回弹滚动效果)相关的知识,希望对你有一定的参考价值。

一.概述

今天主要来分享个自定义viewpagerindicator,效果主要是仿 猫眼电影 顶部的栏目切换,也就是我们常说的indicator,难度简单,为了让滑动时效果更炫酷,我在滑动到左边第一个item或者最右边的item时,添加了滑动到边缘位置后,回弹然后复位的效果(其实也是很简单,只要计算好距离就好啦)
大致的效果图就是这样。大家可以凑合看看(可以看到当滑动到边缘位置的时候有回弹的效果,是不是挺带感的O(∩_∩)O)
技术分享 技术分享

二.使用方法

  1. 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

























以上是关于自定义ViewpagerIndicator (仿猫眼,添加边缘回弹滚动效果)的主要内容,如果未能解决你的问题,请参考以下文章

Jake Wharton之ViewPagerIndicator解读预备役

Android自定义ViewPager图片指示器,兼容实现底部横线指示器

自己定义ViewpagerIndicator (仿猫眼,加入边缘回弹滚动效果)

开源框架ViewPagerIndicator的使用——TabPageIndicator

ViewPagerIndicator的使用方法

如何在Android中使用viewPagerindicator和ViewPager?