github(二)android 模仿tablayout效果的magicindicator源码分析

 前言,这个是用到了自定义控件的源码分析,感觉里面有很多基础的点,有时间觉得这些内容很简单,但是让自己写却是非常困难的,于是对源码进行了分析,这里只是仅仅分析,没有自己去实现,希望看到这篇文章的同学,如果感兴趣,最好自己去实现一遍。

简单了画一个类图,作为开篇,希望对后面的阅读有所帮助。

公共类

   传输类PositionData,这个类是用来记录TextView中屏幕中的位置的,可以实现线的偏移效果,具体代码如下

    /**
     * 控件的左位置
     */
    public int mLeft;
    public int mTop;
    public int mRight;
    public int mBottom;
    /**
     * 文字的左位置
     */
    public int mContentLeft;
    public int mContentTop;
    public int mContentRight;
    public int mContentBottom;

一、首先我们来看SimplePagerTitleView,这个就是下划线上方的文字效果的TextView,

TextView 获取左侧位置,getLeft() + getWidth() / 2 - contentWidth / 2,原理是先获取TextView左侧的位置,然后再加上文字相对TextView左侧的位置,来计算出文字位置屏蔽左侧的位置

  @Override
    public int getContentLeft() {
        Rect bound = new Rect();
        String longestString = "";
        //如果包括了换行,则取换行后最长的长度
        if (getText().toString().contains("\n")) {
            String[] brokenStrings = getText().toString().split("\\n");
            for (String each : brokenStrings) {
                if (each.length() > longestString.length()) longestString = each;
            }
        } else {
            longestString = getText().toString();
        }
        getPaint().getTextBounds(longestString, 0, longestString.length(), bound);
        //计算出文字的宽度
        int contentWidth = bound.width();
        //计算出文字对于的左侧位置
        return getLeft() + getWidth() / 2 - contentWidth / 2;
    }

获取顶部位置,

@Override
    public int getContentTop() {
        Paint.FontMetrics metrics = getPaint().getFontMetrics();
        float contentHeight = metrics.bottom - metrics.top;
        return (int) (getHeight() / 2 - contentHeight / 2);
    }

二、我们这里使用的是ColorTransitionPagerTitleView,这个是可以定义TextView的渐变颜色,on主要是有两个方法构成,一个是onEntry,一个是onLeave,在这两个方法执行时,会调用ArgbEvaluatorHolder,方法,我们来看一下这个方法的代码,通过代码可以看出,通过偏移量来不停的计算出新的颜色,实现颜色的渐变

   //分解开始颜色的色值
        int startA = (startValue >> 24) & 0xff;//获取透明度
        int startR = (startValue >> 16) & 0xff;//获取红色
        int startG = (startValue >> 8) & 0xff;//获取绿色
        int startB = startValue & 0xff; //获取蓝色
        //分解终止颜色的值,原理同上
        int endA = (endValue >> 24) & 0xff;
        int endR = (endValue >> 16) & 0xff;
        int endG = (endValue >> 8) & 0xff;
        int endB = endValue & 0xff;
        //fraction为偏移量
        int currentA = (startA + (int) (fraction * (endA - startA))) << 24;
        int currentR = (startR + (int) (fraction * (endR - startR))) << 16;
        int currentG = (startG + (int) (fraction * (endG - startG))) << 8;
        int currentB = startB + (int) (fraction * (endB - startB));

        //通过【或】运算符拼接出新的颜色
        return currentA | currentR | currentG | currentB;

 三、LinePagerIndicator,这个是就是文字下面的线,这个线的规则有三种模式,分别是MODE_MATCH_EDGE、MODE_WRAP_CONTENT、MODE_EXACTLY三种模式,在onPageScrolled方法中实现线的滚动,和滚动效果

onPageScrolled解析,如果对线条移动感兴趣的小伙伴可以查看我的另一篇文章自定义控件基础之绘制可以滑动的线和可以滚动的textView,在这里详情的分析了如何自己绘制一个可以滑动的线条,以及基础点和难点讲解。

3.1 回弹效果的实现,它是通过FragmentContainerHelper的getImitativePositionData方法来实现计算是不是超出范围

        // 计算锚点位置
        PositionData current = FragmentContainerHelper.getImitativePositionData(mPositionDataList, position);
        PositionData next = FragmentContainerHelper.getImitativePositionData(mPositionDataList, position + 1);

3.2  实现滚动效果 ,是通过修改left和right的值来实现,在这里left和rightx,来控制的,如果position选择是在当前的左边,则leftx就会变小,这时就会向左移动,如果viewpager选择向右滑,则leftx则会变大,向右移动,positionOffset这是一个向右移动的偏移量,则增加移动的速度控件,来实现平滑的移动。在这里需要注意的是,向左移动时,假设A和B,当向A移动时,这时position已经是A的位置了,所以leftX会是A的left,这时线就会向左移动,并不是去减少leftx来实现的。

        mLineRect.left = leftX + (nextLeftX - leftX) * mStartInterpolator.getInterpolation(positionOffset);
        mLineRect.right = rightX + (nextRightX - rightX) * mEndInterpolator.getInterpolation(positionOffset);


四、指示器、CommonNavigator,这是一个把文字、下划线、还有滚动结合起来的一个中继器,用来组合各个独立的控件,主要是由二部分构成,一个是CommonNavigator,还有一个就是CommonNavigatorAdapter。

这个通过初始化方法init()来实现控件的数据的初始刷新,在init方法中可以实现布局的初始化,以及title和indicator的数据填充

4.1 CommonNavigator 中有一个mPositionDataList,这个属性 是用来记录第一个title的位置的,这个方法是在

CommonNavigator的onLayout中执行的,

4.2 在Common在CommonNavigator有一个setAdapter方法,代码如下,在这里,进行了init方法,这个方法用来初始化mTitleContainer方法的,添加了许多的title,在init中通过bringChildToFront,可以调整下滑的位置,可以放到上面,然后是调用

initTitlesAndIndicator方法,来初始化title和indicator方法,来实现子控件的值的初始化。
  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();
        }

4.2.2initTitlesAndIndicator的代码,这里实现了所有title类和线性布局的初始化

 private void initTitlesAndIndicator() {
        for (int i = 0, j = mNavigatorHelper.getTotalCount(); i < j; i++) {
            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);
            }
        }
    }

4.3 onPageScrolled方法,这个方法是整个magicindicator的核心方法,是否实现移动的方法,这里有个小技巧,当我们如果判断一个数超过某个范围时,如果一直取这个值,则使用 Math.min(mPositionDataList.size() - 1, position),即可,这样子也可以实现。mIndicator.onPageScrolled,这个方法就不再介绍了,在第二步有详细说明,在这里需要看一下手指跟随滚动,他是利用scrollview来实现位置不停的实时移动的,这个技巧其实我们在工作中会经常用到的,建议熟练这个用法。

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 实现待选中项完全显示出来
                }
            }
        }
    }

4.4 在onSelected方法中,主要是二个工作,一是设置当前选择文字的颜色,二是实现文字如果遮挡则滚动到不遮挡的位置

   View v = mTitleContainer.getChildAt(index);
        if (v instanceof IPagerTitleView) {
            ((IPagerTitleView) v).onSelected(index, totalCount);
        }
        if (!mAdjustMode && !mFollowTouch && mScrollView != null && mPositionDataList.size() > 0) {
            int currentIndex = Math.min(mPositionDataList.size() - 1, index);
            PositionData current = mPositionDataList.get(currentIndex);
            if (mEnablePivotScroll) {
                float scrollTo = current.horizontalCenter() - mScrollView.getWidth() * mScrollPivotX;
                if (mSmoothScroll) {
                    mScrollView.smoothScrollTo((int) (scrollTo), 0);
                } else {
                    mScrollView.scrollTo((int) (scrollTo), 0);
                }
            } else {
                // 如果当前项被部分遮挡,则滚动显示完全
                if (mScrollView.getScrollX() > current.mLeft) {
                    if (mSmoothScroll) {
                        mScrollView.smoothScrollTo(current.mLeft, 0);
                    } else {
                        mScrollView.scrollTo(current.mLeft, 0);
                    }
                } else if (mScrollView.getScrollX() + getWidth() < current.mRight) {
                    if (mSmoothScroll) {
                        mScrollView.smoothScrollTo(current.mRight - getWidth(), 0);
                    } else {
                        mScrollView.scrollTo(current.mRight - getWidth(), 0);
                    }
                }
            }
        }

   4.5 这里有一个NavigatorHelper类,这是一个CommonNavigator的补充类,主要是用来实现当整体滚动时title的字体颜色的变化,这里有一个点,就是如何实现控件是左移动还是右移动,它是通过位置和偏移量来计算的,比如当从0移动到1时,会执行三次,如果是0,这时会移动到1,执行前二次positioin是0,第三次会变成1,这时判断发生了变化,则记录是移动了。

       dispatchOnLeave和dispatchOnEnter分别是移动的离开和进入的方法,在这两个方法中去实现title的变化操作。

 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        //得到当前位置和偏移量的和
        float currentPositionOffsetSum = position + positionOffset;
        boolean leftToRight = false;
        //判断是左滑还是右滑
        if (mLastPositionOffsetSum <= currentPositionOffsetSum) {
            leftToRight = true;
        }
        //判断状态是不是滚动的状态,这个状态是viewpager的状态传值进来的
        if (mScrollState != ScrollState.SCROLL_STATE_IDLE) {
            //如果位置没有发生变化,则返回
            if (currentPositionOffsetSum == mLastPositionOffsetSum) {
                return;
            }
            int nextPosition = position + 1;
            boolean normalDispatch = true;
            if (positionOffset == 0.0f) {
                if (leftToRight) {
                    nextPosition = position - 1;
                    normalDispatch = false;
                }
            }
            for (int i = 0; i < mTotalCount; i++) {
                if (i == position || i == nextPosition) {
                    continue;
                }

                Float leavedPercent = mLeavedPercents.get(i, 0.0f);
                if (leavedPercent != 1.0f) {
                    dispatchOnLeave(i, 1.0f, leftToRight, true);
                }
            }
            //调用离开和进入的方法,来修改字体的颜色
            if (normalDispatch) {
                if (leftToRight) {
                    dispatchOnLeave(position, positionOffset, true, false);
                    dispatchOnEnter(nextPosition, positionOffset, true, false);
                } else {
                    dispatchOnLeave(nextPosition, 1.0f - positionOffset, false, false);
                    dispatchOnEnter(position, 1.0f - positionOffset, false, false);
                }
            } else {
                dispatchOnLeave(nextPosition, 1.0f - positionOffset, true, false);
                dispatchOnEnter(position, 1.0f - positionOffset, true, false);
            }
        } else {
            for (int i = 0; i < mTotalCount; i++) {
                if (i == mCurrentIndex) {
                    continue;
                }
                boolean deselected = mDeselectedItems.get(i);
                if (!deselected) {
                    dispatchOnDeselected(i);
                }
                Float leavedPercent = mLeavedPercents.get(i, 0.0f);
                if (leavedPercent != 1.0f) {
                    dispatchOnLeave(i, 1.0f, false, true);
                }
            }
            dispatchOnEnter(mCurrentIndex, 1.0f, false, true);
            dispatchOnSelected(mCurrentIndex);
        }
        //完成后重新赋值
        mLastPositionOffsetSum = currentPositionOffsetSum;
    }

五、CommonNavigatorAdapter的实现,这个是获取控件(非绑定数据)的适配器,具体介绍如下

  5.1 适配器的必备定义了一个DataSetObservable,实现了观察者模式,用来实现被观察的数据

  5.2 获取实现数据的布局,和数量,在这里使用了getCount、getTitleView、getIndicator,这三个方法实现了对 文本和标签的数据抽象,

public abstract class CommonNavigatorAdapter {

    private final DataSetObservable mDataSetObservable = new DataSetObservable();

    public abstract int getCount();

    public abstract IPagerTitleView getTitleView(Context context, int index);

    public abstract IPagerIndicator getIndicator(Context context);

    public float getTitleWeight(Context context, int index) {
        return 1;
    }

    public final void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    public final void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    public final void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

    public final void notifyDataSetInvalidated() {
        mDataSetObservable.notifyInvalidated();
    }
}

 5.3 定义了一个DataSetObserver变量,这是一个观察者,当DataSetObservable执行notifyChanged()方法时,会通过DataObserver发生了变化。这时会执行DataSetObserver的onChanged()方法,这时再进行,在这里可以清空布局的控件,对获取的布局重新更新


    private DataSetObserver mObserver = new DataSetObserver() {

        @Override
        public void onChanged() {
            mNavigatorHelper.setTotalCount(mAdapter.getCount());    // 如果使用helper,应始终保证helper中的totalCount为最新
            init();
        }

        @Override
        public void onInvalidated() {
            // 没什么用,暂不做处理
        }
    };

5.4 通过init方法引入布局,这里使用的填充布局LayoutInflater,定义一个FrameLayout,直接填充布局,完成布局的实现,

  private void init(){
        View root;
        root= LayoutInflater.from(getContext()).inflate(R.layout.activity_imagesize,this);
    }

5.5在 onLayout中计算各个控件的坐标

private void preparePositionData() {
        mPositionDataList.clear();
        for (int i = 0, j = mNavigatorHelper.getTotalCount(); i < j; i++) {
            PositionData data = new PositionData();
            View v = mTitleContainer.getChildAt(i);
            if (v != null) {
                data.mLeft = v.getLeft();
                data.mTop = v.getTop();
                data.mRight = v.getRight();
                data.mBottom = v.getBottom();
                if (v instanceof IMeasurablePagerTitleView) {
                    IMeasurablePagerTitleView view = (IMeasurablePagerTitleView) v;
                    data.mContentLeft = view.getContentLeft();
                    data.mContentTop = view.getContentTop();
                    data.mContentRight = view.getContentRight();
                    data.mContentBottom = view.getContentBottom();
                } else {
                    data.mContentLeft = data.mLeft;
                    data.mContentTop = data.mTop;
                    data.mContentRight = data.mRight;
                    data.mContentBottom = data.mBottom;
                }
            }
            mPositionDataList.add(data);
        }
    }

5.6 在设置adapter时,是在CommonNavigator的setAdapter方法中实现的,可以在这里更新控件,并且DataObserver是唯一的,每次都是取消注册上一个,换成新的adapter。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Binder是Android操作系统中的一个IPC(进程间通信)机制,用于实现进程之间的通信和数据传输。Binder的源码主要位于frameworks/native目录下。 在Binder的源码中,最核心的部分是Binder驱动和Binder服务。Binder驱动是位于内核空间的组件,负责处理进程间的数据传输和交互。Binder服务是位于用户空间的组件,负责提供接口和功能来进行进程间通信。 在Binder的源码中,主要涉及到以下几个重要的文件和目录: 1. drivers目录:包含了Binder驱动的代码,其中最重要的文件是binder.c,它实现了Binder驱动的核心逻辑。 2. include目录:包含了Binder的头文件,其中最重要的文件是binder.h,它定义了Binder的接口和数据结构。 3. libbinder目录:包含了Binder服务的代码,其中最重要的文件是IBinder.cpp和BpBinder.cpp,它们分别实现了Binder服务的接口和代理类。 4. services目录:包含了一些系统级别的Binder服务,例如Package Manager Service和Activity Manager Service。 如果你想深入了解Android Binder的源码,可以参考以下资源: 1. Android 源码:你可以从Android官网或者GitHub上获取Android源码,并在frameworks/native目录下查看Binder相关的代码。 2. Android系统架构指南:Android官网提供了关于Android系统架构的详细文档,其中有关IPC和Binder的章节对于理解Binder的实现原理和源码结构很有帮助。 3. 《深入理解Android:卷2》一书中有关于Binder的详细介绍和源码解析,可以作为参考资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值