Android从零开搞系列:自定义View(5)基本的自定义ViewPager指示器+开源项目分析(下)

开源ViewPager指示器分析

转载请注意:http://blog.csdn.net/wjzj000/article/details/53664920

我和一帮应届生同学维护了一个公众号:IT面试填坑小分队。旨在帮助应届生从学生过度到开发者,并且每周树立学习目标,一同进步!
这里写图片描述


开始之前,先看一波效果

这里写图片描述

项目里包含了特别多的效果和细节之处。在这里就只分析上图中涵盖的效果…

本项目原GitHub地址:https://github.com/hackware1993/MagicIndicator

先看一看这个Activity中包含的内容

看之前,我们来拆分这个效果
ViewPager+多个指示器+自定义的ViewPager适配器

现在分析之前-各类之间的关系

  • MagicIndicator:此类用于绑定到ViewPager监听滑动事件,然后对事件转发至CommonNavigator类之上。并且绑定在CommonNavigator中,调用CommonNavigator中的初始化方法。
  • CommonNavigator:真正加载Tab标题以及View效果的类。此类与RecyclerView类似,一定要以RecyclerView的思想去想象!(此处涉及到观察者模式。有兴趣可以查看我的另一篇博客:

  • IPagerIndicator:接口类,由CommonNavigator实现。

  • IPagerTitleView:TabView的接口类,由具体的Tab继承并实现对应方法。
  • CommonNavigatorAdapter:抽象类,CommonNavigator的设配器。

指示器的相关代码

作者在这里为了方便的使用不同的Tab效果,并没有使用我们通常的直接自定义指示器类。
而是通过一个Adapter把其中的Item作为指示器的效果。就像我们使用RecyclerView的那样,每一种Item代表一种样式。而这里也是这样的思想。

//MagicIndicator:指示器类。下文有此类的详细分析
MagicIndicator magicIndicator = (MagicIndicator) findViewById(R.id.magic_indicator1);
        magicIndicator.setBackgroundColor(Color.parseColor("#d43d3d"));
        //CommonNavigator:(类比RecyclerView)此类用于装填Tab的标题和指示View。
        CommonNavigator commonNavigator = new CommonNavigator(this);
        commonNavigator.setSkimOver(true);
        int padding = UIUtil.getScreenWidth(this) / 2;
        commonNavigator.setRightPadding(padding);
        commonNavigator.setLeftPadding(padding);
        //此写法对应我们RecyclerView中Adapter的写法,思想是Adapter中的每一个Item就是对应一个指示器的效果。这样可以很方便的替换指示器。
        //此适配器在下文被拿出,详细分析。
        commonNavigator.setAdapter(new CommonNavigatorAdapter() {
            @Override
            public int getCount() {
                return mDataList == null ? 0 : mDataList.size();
            }

            @Override
            public IPagerTitleView getTitleView(Context context, final int index) {
                //这里就是自定义的View,即指示器的真正效果
                ClipPagerTitleView clipPagerTitleView = new ClipPagerTitleView(context);
                clipPagerTitleView.setText(mDataList.get(index));
                clipPagerTitleView.setTextColor(Color.parseColor("#f2c4c4"));
                clipPagerTitleView.setClipColor(Color.WHITE);
                clipPagerTitleView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mViewPager.setCurrentItem(index);
                    }
                });
                return clipPagerTitleView;
            }

            @Override
            public IPagerIndicator getIndicator(Context context) {
                return null;
            }
        });

        magicIndicator.setNavigator(commonNavigator);
        //一个ViewPager的辅助类,和第一部分setViewPager的效果是一样的。
        ViewPagerHelper.bind(magicIndicator, mViewPager);

MagicIndicator类

此类的分析以写在注释之中。
此类仅仅用于分发ViewPager的回调事件和初始化CommonNavigator。
View的绘制在CommonNavigator类中。(一定要记住这个类和RecyclerView极为相似)

    public class MagicIndicator extends FrameLayout {
    /**
     * 梳理:
     * 此类的作用,用于转发各种事件。
     * 最终的调用在ViewPager的监听中,我们Tab随ViewPager的变化而变化。
     * ViewPagerHelper会沟通ViewPager和MagicIndicator。
     * 通过ViewPager的addOnPageChangeListener()监听在合适的位置调用本类方法。
     * 然后本类进行事件转发,调用CommonNavigator中的特定方法。
     */
    //此类的具体实现为CommonNavigator
    private IPagerNavigator mNavigator;

    public MagicIndicator(Context context) {
        super(context);
    }

    public MagicIndicator(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    //方法回调
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mNavigator != null) {
            mNavigator.onPageScrolled(position, positionOffset, positionOffsetPixels);
        }
    }
    //方法绑定
    public void onPageSelected(int position) {
        if (mNavigator != null) {
            mNavigator.onPageSelected(position);
        }
    }
    //方法绑定
    public void onPageScrollStateChanged(int state) {
        if (mNavigator != null) {
            mNavigator.onPageScrollStateChanged(state);
        }
    }
    //此方法并没有在项目中用到
    public IPagerNavigator getNavigator() {
        return mNavigator;
    }
    //此方法传入的值为:CommonNavigator(继承IPagerNavigator)
    public void setNavigator(IPagerNavigator navigator) {
        //判空
        if (mNavigator == navigator) {
            return;
        }
        //如果不为空先取消关联,并移除其中所有子View,重新关联与初始化。
        if (mNavigator != null) {
            mNavigator.onDetachFromMagicIndicator();
        }
        mNavigator = navigator;
        removeAllViews();
        //类型判断
        if (mNavigator instanceof View) {
            /**
             * 将mNavigator加入到自己的内部。
             * mNavigator是CommonNavigator,这个类是类似RecyclerView,真正的Tab显示
             * 就在其中(注意想象RecyclerView与Adapter配合实现的效果)。
             */
            LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            addView((View) mNavigator, lp);
            /**
             * 此方法最终会调用CommonNavigator的初始化方法,完成对CommonNavigator的布局加载。
             * 此布局是一个垂直排放的俩个线性布局。也就是上边的显示文字效果,下边是导航指示。
             */
            mNavigator.onAttachToMagicIndicator();
        }
    }
}

CommonNavigator类

此类的解析已经写在注释之中。
此类比较完善且复杂所以这里只摘取其中部分。

public void setAdapter(CommonNavigatorAdapter adapter) {
        //判空以及取消注册并重新注册
        if (mAdapter == adapter) {
            return;
        }
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mObserver);
        }
        mAdapter = adapter;
        if (mAdapter != null) {
            mAdapter.registerDataSetObserver(mObserver);
            mNavigatorHelper.setTotalCount(mAdapter.getCount());
            if (mTitleContainer != null) {
                // adapter改变时,应该重新init,但是第一次设置adapter不用,onAttachToMagicIndicator中有init
                mAdapter.notifyDataSetChanged();
            }
        } else {
            mNavigatorHelper.setTotalCount(0);
            init();
        }
    }

初始化自身布局

/**
     * 布局加载,初始化各个控件
     * 布局效果:一个可以水平滑动的Layout然后垂直排放俩个线性布局。
     */
    private void init() {
        removeAllViews();

        View root;
        if (mAdjustMode) {
            root = LayoutInflater.from(getContext()).inflate(R.layout.pager_navigator_layout_no_scroll, this);
        } else {
            root = LayoutInflater.from(getContext()).inflate(R.layout.pager_navigator_layout, this);
        }
        // mAdjustMode为true时,mScrollView为null
        mScrollView = (HorizontalScrollView) root.findViewById(R.id.scroll_view);

        mTitleContainer = (LinearLayout) root.findViewById(R.id.title_container);
        mTitleContainer.setPadding(mLeftPadding, 0, mRightPadding, 0);

        mIndicatorContainer = (LinearLayout) root.findViewById(R.id.indicator_container);
        if (mIndicatorOnTop) {
            mIndicatorContainer.getParent().bringChildToFront(mIndicatorContainer);
        }
        //初始化title和indicator
        initTitlesAndIndicator();
    }

初始化标题和指示效果

private void initTitlesAndIndicator() {
        for (int i = 0, j = mNavigatorHelper.getTotalCount(); i < j; i++) {
            /**
             * 最终我们会在Activity之中通过CommonNavigator.setAdapter来new一个CommonNavigatorAdapter
             * 因此会实现对应的方法,而在对应的方法中我们会初始化继承IPagerTitleView的
             * 自定义的View,也就是我们各式各样的适配器。
             * 此处是标题View的添加,下面是指示条的添加。二者对应布局中的俩个线性布局
             */
            IPagerTitleView v = mAdapter.getTitleView(getContext(), i);
            if (v instanceof View) {
                View view = (View) v;
                LinearLayout.LayoutParams lp;
                if (mAdjustMode) {
                    lp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
                    lp.weight = mAdapter.getTitleWeight(getContext(), i);
                } else {
                    lp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
                }
                mTitleContainer.addView(view, lp);
            }
        }
        if (mAdapter != null) {
            //此处的解释和上边一样。俩这配合完成最终的标题加自定义指示的效果
            mIndicator = mAdapter.getIndicator(getContext());
            if (mIndicator instanceof View) {
                LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
                mIndicatorContainer.addView((View) mIndicator, lp);
            }
        }
    }

PagerView滚动后,于此对应的Tab滑动

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mAdapter != null) {

            mNavigatorHelper.onPageScrolled(position, positionOffset, positionOffsetPixels);
            if (mIndicator != null) {
                mIndicator.onPageScrolled(position, positionOffset, positionOffsetPixels);
            }

            // 手指跟随滚动
            if (mScrollView != null && mPositionDataList.size() > 0 && position >= 0 && position < mPositionDataList.size()) {
                if (mFollowTouch) {
                    int currentPosition = Math.min(mPositionDataList.size() - 1, position);
                    int nextPosition = Math.min(mPositionDataList.size() - 1, position + 1);
                    PositionData current = mPositionDataList.get(currentPosition);
                    PositionData next = mPositionDataList.get(nextPosition);
                    float scrollTo = current.horizontalCenter() - mScrollView.getWidth() * mScrollPivotX;
                    float nextScrollTo = next.horizontalCenter() - mScrollView.getWidth() * mScrollPivotX;
                    mScrollView.scrollTo((int) (scrollTo + (nextScrollTo - scrollTo) * positionOffset), 0);
                } else if (!mEnablePivotScroll) {
                    // TODO 实现待选中项完全显示出来
                }
            }
        }
    }

此类着实比较复杂,一俩句话真心理不全,因为作者考虑了拓展问题。因为引入了不少的类。功能使用方便很强势,但是着实不好理解。
但是总结一下,这类用于真正接受标题和指示器效果然后绘制到自身的布局之上。
并且回调相关方法,完成ViewPager的滚动效果。是自身的Tab也能够完成移动。


ClipPagerTitleView类

相关分析写在注释。代码比较长,摘取部分的。
自定义的View类,在这里我们只需要根据自己的需求,在对应通用接口中完成效果即可。

    /**
     * 重写onMeasure(),使其可以支持wrap_content以及padding效果。
     * 固定解决方案。
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureTextBounds();
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }
    //使其可以支持wrap_content以及padding效果。
    private int measureWidth(int widthMeasureSpec) {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        int result = size;
        switch (mode) {
            case MeasureSpec.AT_MOST:
                int width = mTextBounds.width() + getPaddingLeft() + getPaddingRight();
                result = Math.min(width, size);
                break;
            case MeasureSpec.UNSPECIFIED:
                result = mTextBounds.width() + getPaddingLeft() + getPaddingRight();
                break;
            default:
                break;
        }
        return result;
    }
    @Override
    protected void onDraw(Canvas canvas) {
        int x = (getWidth() - mTextBounds.width()) / 2;
        int y = (getHeight() + mTextBounds.height()) / 2;

        // 画底层
        mPaint.setColor(mTextColor);
        canvas.drawText(mText, x, y, mPaint);

        // 画clip层
        canvas.save(Canvas.CLIP_SAVE_FLAG);
        if (mLeftToRight) {
            canvas.clipRect(0, 0, getWidth() * mClipPercent, getHeight());
        } else {
            canvas.clipRect(getWidth() * (1 - mClipPercent), 0, getWidth(), getHeight());
        }
        mPaint.setColor(mClipColor);
        canvas.drawText(mText, x, y, mPaint);
        canvas.restore();
    }

  • Ok,最后再梳理一下大概的流程。首先由一个设计上类似RecyclerView的CommonNavigator类,负责Tab的加载以及滑动Tab已达到和ViewPager同步的效果。
  • MagicIndicator类用于分发ViewPager的滚动事件到CommonNavigator上。
  • 当然为了更好的拓展性,作者抽象了很多接口,抽象类以及Helper类。这其中的乐趣需要我们自己去体会。

PS:相关源码基本都存放于我的这个开源项目之中:
https://github.com/zhiaixinyang/PersonalCollect


最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值