仿IPhone滚轮组件分析WheelScroller

源码

https://github.com/chemalarrea/Android-wheel

效果图

仿IPhone滚轮组件分析WheelScroller_第1张图片

代码实现:wheelScroller

基本实现过程:

UML结构图

仿IPhone滚轮组件分析WheelScroller_第2张图片

源码分析

滚动的实体

滚轮中间滚动的内容是?
LinearLayout WheelView.itemsLayout
该布局中的子View就是我们所要滚动选择的内容

滚动的原理

在滑动过程中,根据每次onTouchEvent回调来的Y坐标差值delta ,来不断更新相关属性值,这是View绘制位置的数据来源。

WheelView.java

    private void doScroll(int delta) {
        /** * 在滑动过程中会源源不断的传递delta过来, * 从手按下到手抬起后,scrollingOffset记录了这个阶段总的滑动量 **/
        scrollingOffset += delta;

        int itemHeight = getItemHeight();
        /** * 刚才的滑动经历了几个item, * 注意count是带符号的,下正,上负 **/
        int count = scrollingOffset / itemHeight;

        /** * 当前的位置减去count得到当前的位置。 * 为什么减呢?如果你向下滑动的话,其实操作者的意思是滑倒表头, * count为正,列表的pos当然是在减少。同理向上滑动则pos增加 **/
        int pos = currentItem - count;
        int itemCount = viewAdapter.getItemsCount();

        /** * 不可能滑动的时候都是那么恰好滑动整个item的距离。 * 那么如果滑动item高度的整数倍还多出来那么一点(求模)该怎么认定 * 呢?如果多出来的一点超过了单个item的一半高度,即认为它应该再增 * 加一个pos,否则就不增加了,fixPos 因为scrollingOffset所以它 * 也是有符号的。 **/
        int fixPos = scrollingOffset % itemHeight;
        if (Math.abs(fixPos) <= itemHeight / 2) {
            fixPos = 0;
        }
        if (isCyclic && itemCount > 0) {
            if (fixPos > 0) {
                pos--;
                count++;
            } else if (fixPos < 0) {
                pos++;
                /** *为什么这里count是自减呢,因为count是带符号的,此时 *它<0,自减实际上使得其绝对值增加 **/
                count--;
            }
            // fix position by rotating
            while (pos < 0) {
                pos += itemCount;
            }
            pos %= itemCount;
        } else {
            //
            if (pos < 0) {
                count = currentItem;
                pos = 0;
            } else if (pos >= itemCount) {
                count = currentItem - itemCount + 1;
                pos = itemCount - 1;
            } else if (pos > 0 && fixPos > 0) {
                pos--;
                count++;
            } else if (pos < itemCount - 1 && fixPos < 0) {
                pos++;
                count--;
            }
        }

        int offset = scrollingOffset;
        if (pos != currentItem) {
            /* * 主要工作就是将上面计算好的pos赋值给全局currentItem,通知 * 监听者pos发生改变并触发重绘。 **/
            setCurrentItem(pos, false);
        } else {
            invalidate();
        }

        /** * 这么一减,scrollingOffset存放的就是与itemHeight的整数倍差值 * 了。 * scrollingOffset 的身份在doScroll函数中一直变化,所以不太好 * 理解。因为在滚动中,要求是整个整个的item滑动,但是实际中的滑动 * 不可能做到这一点,肯定会和整数个item的距离有一定的偏差,那么这 * 个偏差就是scrollingOffset。count * itemHeight为最终要移动 * 的整数个item的距离 */
        scrollingOffset = offset - count * itemHeight;
        if (scrollingOffset > getHeight()) {
            scrollingOffset = scrollingOffset % getHeight() + getHeight();
        }
    }

理解参见 图一
仿IPhone滚轮组件分析WheelScroller_第3张图片

在上面的计算完毕后,触发重绘,而重绘所利用到的数据就是基于上面的计算doScroll函数执行的结果。

 private void drawItems(Canvas canvas) {
        canvas.save();

        if(hasSpecial) {
            for (int i = 0; i < itemsLayout.getChildCount(); i++) {
                View view = itemsLayout.getChildAt(i);
                if (view instanceof TextView) {
                    centerTextView = (TextView) view;
                } else if (itemTextResourceId > 0) {
                    centerTextView = ((TextView) view.findViewById(itemTextResourceId));
                }
                if (centerTextView != null) {
                    centerTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((Integer)view.getTag()) == currentItem ? centerTextSize : defaultTextSize);
                    centerTextView.setTextColor(((Integer)view.getTag()) == currentItem ? centerTextColor : defaultTextColor);
                }
            }
        }

        /** * 当被问起,如何随着手的滑动,View也随之滑动改变呢? * 将canvas的位置移动,并且在移动后的地方绘制 * (currentItem - firstItem) * getItemHeight() 是当前列 * 表相对于起初的位置要移动的距离量,(getItemHeight() - * getHeight()) / 2 则是初始位置,因为我们的起始位置是在纵向中间 * 的位置,而不是在左上角,计算见图二。 */
        int top = (currentItem - firstItem) * getItemHeight() + (getItemHeight() - getHeight()) / 2;
        canvas.translate(0, - top + scrollingOffset);

        itemsLayout.draw(canvas);

        canvas.restore();
    }

理解参见 图二

上面代码的分析基本上能够说明了该组件的最基础原理,根据手滑动的Y轴位移来不停的reDraw 各个Item.

问&答:

1.fling后的过程是如何实现的?

在WheelScroller.java中
在每次有 按下事件发生的时候,GestureDetector都会判断一下。

public boolean onTouchEvent(MotionEvent event) {
    \**some code omitted here**\
    if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
            justify();
        }

注册手势监听

// gesture listener
    private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // Do scrolling in onTouchEvent() since onScroll() are not call immediately
            // when user touch and move the wheel
            return true;
        }

        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            lastScrollY = 0;
            final int maxY = 0x7FFFFFFF;
            final int minY = -maxY;
            scroller.fling(0, lastScrollY, 0, (int) -velocityY, 0, 0, minY, maxY);
            setNextMessage(MESSAGE_SCROLL);
            return true;
        }
    };

在GestureDetector组件识别到有fling手势的时候,在回调中调用
scroller.fling的方法。
NOTE:scroller.fling专门为fling 的 gesture打造,根据GestureDetector返回的fling速度来自动调整惯性滑动的距离及时间。速度越快,惯性滑动的距离和时间就越长,

再来看看handler 是如何处理的。

private Handler animationHandler = new Handler() {
        public void handleMessage(Message msg) {
            /** * Scroller不负责和View交互,只是提供了滑动时候的位置移动 * 位置支持。所以我们要通过Scroller得到当前应该在的“位置” * ,因为它提供的位置信息并不是基于真实的坐标,所以将它仍旧变 * 成位移增量delta.拿到delta后再交给WheelView去执行真正的 * View scroll. **/
            scroller.computeScrollOffset();
            int currY = scroller.getCurrY();
            int delta = lastScrollY - currY;
            lastScrollY = currY;
            if (delta != 0) {
                listener.onScroll(delta);
            }

            // scrolling is not finished when it comes to final Y
            // so, finish it manually
            if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {
                currY = scroller.getFinalY();
                scroller.forceFinished(true);
            }

            /** *当Scroller还没有结束的时候,我们需要持续不断的从它这里获取 *计算后的位置相对值,所以这里会继续发送消息以保持持续的运转 **/
            if (!scroller.isFinished()) {
                animationHandler.sendEmptyMessage(msg.what);
            } else if (msg.what == MESSAGE_SCROLL) {
                justify();
            } else {
                finishScrolling();
            }
        }
    };

2.进入中间的“选择带”较多则会被“吸”到显示带中,如果进入较少则会被“吸出去”,这又是如何实现的。

可以知道的是,它和listView重要的区别点在于此。
在WheelScroller.java中
在每次有 触摸事件发生的时候,都会判断一下是否是手指离开的操作。如果是的话就执行justify()函数

public boolean onTouchEvent(MotionEvent event) {
    \**some code omitted here**\
    if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
            justify();
        }

它干了些什么呢?

private void justify() {
        listener.onJustify();
        setNextMessage(MESSAGE_JUSTIFY);
    }

做了两件事情

一.回调onJustify()

public void onJustify() {
            if (Math.abs(scrollingOffset) > WheelScroller.MIN_DELTA_FOR_SCROLLING) {
                scroller.scroll(scrollingOffset, 0);
            }
        }

关键的地方是scrollingOffset,在前面的(源码分析–>滚动原理)中提到

/**
* 这么一减,scrollingOffset存放的就是与itemHeight的count整数倍差值了。
*/
scrollingOffset = offset - count * itemHeight;

scrollingOffset 的身份在doScroll函数中一直变化,所以不太好理解。
因为在滚动中,要求是整个整个的item滑动,但是实际中的滑动不可能做到这一点,肯定会和整数个item的距离有一定的偏差,那么这个偏差就是scrollingOffset。
count * itemHeight为最终要移动的整数个item的距离

最后调用的是:

public void scroll(int distance, int time) {
        scroller.forceFinished(true);

        lastScrollY = 0;

        scroller.startScroll(0, 0, 0, distance, time != 0 ? time : SCROLLING_DURATION);
        setNextMessage(MESSAGE_SCROLL);

        startScrolling();
    }

也就是说最后,它也是借助Scroller.startScroll方法来 然后调用setNextMessage(MESSAGE_SCROLL)来使得View scroll的,后面的过程跟fling的过程没什么区别。

二.给handler发MESSAGE_JUSTIFY

其实这个发送可有可无。

3.显示带内的Text是黑色,而之外的则是灰色,如何实现的呢?

 private void drawItems(Canvas canvas) {
        canvas.save();

        if(hasSpecial) {
            for (int i = 0; i < itemsLayout.getChildCount(); i++) {
                View view = itemsLayout.getChildAt(i);
                if (view instanceof TextView) {
                    centerTextView = (TextView) view;
                } else if (itemTextResourceId > 0) {
                    centerTextView = ((TextView) view.findViewById(itemTextResourceId));
                }
                if (centerTextView != null) {
                    centerTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((Integer)view.getTag()) == currentItem ? centerTextSize : defaultTextSize);
                    centerTextView.setTextColor(((Integer)view.getTag()) == currentItem ? centerTextColor : defaultTextColor);
                }
            }
        }

        int top = (currentItem - firstItem) * getItemHeight() + (getItemHeight() - getHeight()) / 2;
        canvas.translate(0, - top + scrollingOffset);

        itemsLayout.draw(canvas);

        canvas.restore();
    }

在绘制的时候关键的两句

centerTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((Integer)view.getTag()) == currentItem ? centerTextSize : defaultTextSize);
                    centerTextView.setTextColor(((Integer)view.getTag()) == currentItem ? centerTextColor : defaultTextColor);

判断当前的view是否为currentItem,如果是则区别显示。

currentItem如何判断的?

可通过前面的WheelView.doScroll()中的解析了解到会根据移动的距离来计算出来当前在中间位置所对应的索引值是多少。

问题

在滑动的时候,内容会向左偏移一下。

你可能感兴趣的:(仿IPhone滚轮组件分析WheelScroller)