zoukankan      html  css  js  c++  java
  • 自己定义ViewpagerIndicator (仿猫眼,加入边缘回弹滚动效果)

    一.概述

    今天主要来分享个自己定义viewpagerindicator。效果主要是仿 猫眼电影 顶部的栏目切换。也就是我们常说的indicator,难度简单,为了让滑动时效果更炫酷,我在滑动到左边第一个item或者最右边的item时,加入了滑动到边缘位置后,回弹然后复位的效果(事实上也是非常easy,仅仅要计算好距离就好啦)
    大致的效果图就是这样。

    大家能够凑合看看(能够看到当滑动到边缘位置的时候有回弹的效果,是不是挺带感的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
    欢迎star。fork!

    `(∩_∩)′

  • 相关阅读:
    oracle 自动备份,删除历史
    oracle 系统语法
    myeclipse快捷键
    ANT教程
    新手 Spring 疑惑
    MyEclipse8.5整合Git (转)
    eclipse build path功能总结
    Apache Maven 入门篇(下)
    Mac OS下配置PHP Nginx PHP-FPM
    Python删除列表中元素
  • 原文地址:https://www.cnblogs.com/gccbuaa/p/7140709.html
Copyright © 2011-2022 走看看