Android仿新版微信的小程序下拉栏

作者: 大头呆 | 来源:发表于2018-01-04 15:59 被阅读645次

    上周微信更新到了6.6.1版本,加入了微信小游戏。朋友圈都在玩跳一跳。而且现在微信把最近用过的小程序放到了首页顶部,轻轻下拉就可以快速访问了。可以看下效果,如果还没升级的朋友可要抓紧了。

    extend.gif

    自己作为一个安卓程序员,虽然不会写小程序,但也要紧跟热潮(蹭热点)啊。于是乎就干脆仿写下这个下拉控件吧。第七宇宙惯例,先上效果图:

    header.gif

    上图的主界面是我上一篇文章《撸一款全手势操作浏览器》写的demo。写完这个控件,发现正好可以作为它的下拉菜单栏,就直接用上了。好了,废话不多说,开始介绍下实现流程。

    流程分析

    整个下拉过程分为四个阶段:

    • 阶段一:出现一个圆点,半径随下拉距离变大而变大。位置始终在中间
    • 阶段二:圆点两边出现两个圆点,半径较小。距离随下拉距离变大而变大,中间圆点半径不断变小。位置始终在中间
    • 阶段三:从顶部出现内容列表,位置随手指下拉快速往下移动,同时三个圆点位置不断下移并逐渐消失
    • 阶段四:只剩下内容列表,手指可以继续往下滑动,但阻尼变大。内容列表始终在中间。

    上滑分两种情况:

    • 如果开始上滑的时候内容列表已展开,则平移上滑(圆点不会出现)
    • 反之,就是下拉的逆过程了(圆点会出现)。

    具体实现

    熟悉下拉刷新控件的同学可以看出来,上述滑动的流程和下拉刷新很相似,所以为了避免重复造轮子(偷懒),我将下拉刷新控件作了改动,所以主要的实现还是在头部那块。

    初始布局位置

    将头部放到屏幕外层的方法有很多。我采用了设置负数padding的方法。外层布局继承了LinearLayout,方向竖直。然后为其设置padding:

    headerHeight = (null != mHeaderLayout) ? mHeaderLayout.getMeasuredHeight() : 0;
    int pLeft = getPaddingLeft();
    int pTop = -headerHeight;
    int pRight = getPaddingRight();
    int pBottom = -footerHeight;
    setPadding(pLeft, pTop, pRight, pBottom);
    

    paddingTop的值等于负的HeaderLayout的高度,这样正好将头部布局顶到屏幕外面。

    处理触摸事件

    这块主要内容就是重写写 boolean onInterceptTouchEvent(MotionEvent event)boolean onTouchEvent(MotionEvent ev)来拦截和处理滑动事件。

      @Override
        public final boolean onInterceptTouchEvent(MotionEvent event) {
            final int action = event.getAction();
            //不拦截
            if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                mIsHandledTouchEvent = false;
                return false;
            }
            //如果不是重新开始触摸且已经判断需要拦截,就一直拦截整套触摸事件
            if (action != MotionEvent.ACTION_DOWN && mIsHandledTouchEvent) {
                return true;
            }
    
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastMotionY = event.getY();
                    mIsHandledTouchEvent = false;
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    final float deltaY = event.getY() - mLastMotionY;
                    final float absDiff = Math.abs(deltaY);
                    // 位移差大于mTouchSlop(TouchSlop是系统所能识别出的被认为是滑动的最小距离)
                    //这是为了防止快速拖动引发刷新
                    if ((absDiff > mTouchSlop)) {
                        mLastMotionY = event.getY();
                        // 第一个显示出来,Header已经显示或拉下
                        if (isPullRefreshEnabled() && isReadyForPullDown()) {
                            // 1,Math.abs(getScrollY()) > 0:表示当前滑动的偏移量的绝对值大于0,表示当前HeaderView滑出来了或完全
                            // 不可见,存在这样一种case,当正在刷新时并且RefreshableView已经滑到顶部,向上滑动,那么我们期望的结果是
                            // 依然能向上滑动,直到HeaderView完全不可见
                            // 2,deltaY > 0.5f:表示下拉的值大于0.5f
                            mIsHandledTouchEvent = (Math.abs(getScrollYValue()) > 0 || deltaY > 0.5f);
                        }
                    }
                    break;
                default:
                   break;
            }
            return mIsHandledTouchEvent;//true:拦截,false不拦截
        }
    

    如果拦截了,我们处理滑动.其中offsetRadio是滑动阻尼值。

    @Override
        public final boolean onTouchEvent(MotionEvent ev) {
            boolean handled = false;
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mLastMotionY = ev.getY();
                    mIsHandledTouchEvent = false;
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    final float deltaY = ev.getY() - mLastMotionY;
                    mLastMotionY = ev.getY();
                    if (isPullRefreshEnabled() && isReadyForPullDown()) {
                        pullHeaderLayout(deltaY / offsetRadio);
                        handled = true;
                    } else {
                        mIsHandledTouchEvent = false;
                    }
                    break;
    
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    if (mIsHandledTouchEvent) {
                        mIsHandledTouchEvent = false;
                        // 当第一个显示出来时
                        if (isReadyForPullDown()) {
                            // 调用刷新
                            if (mPullRefreshEnabled && (mPullDownState == State.RELEASE_TO_REFRESH)) {
                                startRefreshing();
                                handled = true;
                            }
                            resetHeaderLayout();
                        } 
                    }
                    break;
    
                default:
                    break;
            }
            return handled;
        }
    
    

    最终我们会通过 pullHeaderLayout()调用View的scrollBy(x, y)方法将布局整体滚动:

     protected void pullHeaderLayout(float delta) {
            // 向上滑动,并且当前scrollY为0时,不滑动
            int oldScrollY = getScrollYValue();
            if (delta < 0 && (oldScrollY - delta) >= 0) {
                setScrollTo(0, 0);
                if (null != mHeaderLayout && 0 != mHeaderHeight) {
                    mHeaderLayout.setState(State.RESET);
                    mHeaderLayout.onPull(0);
                }
                return;
            }
            //滑动布局
            setScrollBy(0, -(int) delta);//调用View的 scrollBy(x, y)
            int scrollY = Math.abs(getScrollYValue());
            if (null != mHeaderLayout && 0 != mHeaderHeight) {
                if (scrollY >= headerListHeight) {
                    mHeaderLayout.setState(State.arrivedListHeight);
                    setOffsetRadio(2.0f);//内容列表完全展开后阻尼值变大
                } else {
                    setOffsetRadio(1.0f);
                }
                mHeaderLayout.onPull(scrollY);//将滑动距离实时传给头部,以实现出我们需要的动画
            }
        }
    

    遇到的一个问题

    我在上面这样写好之后,跑了一遍,发现 onInterceptTouchEvent只执行了ACTION_DOWN,后续的ACTION_MOVE和ACTION_UP事件不会执行,也就无法拦截和进行滑动了。百度下原来有这么一个规则:

    onInterceptTouchEvent返回false表示将down事件交由子View来处理;若某一层子View的onTouchEvent返回了true,后续的move、up等事件都将先传递到ViewGroup的onInterceptTouchEvent的方法,并继续层层传递下去,交由子View处理;若子View的onTouchEvent都返回了false,则down事件将交由该ViewGroup的onTouchEvent来处理;如果ViewGroup的onTouchEvent返回true,后续事件不再经过该ViewGroup的onInterceptTouchEvent方法,直接传递给onTouchEvent方法处理。

    因为目前的子View(中间内容部分)是RelativeLayout,它的onTouchEvent默认返回了false(ListView等其他可滑动的控件不会有这个问题)。解决办法是设置android:clickable="true"

    头部的实现

    上面我说过滑动有四个阶段,只要将滑动距离传递给自定义头部,根据距离判断状态,实时改变内容列表的TranslationY,圆点的TranslationYAlpha就可以了。实现起来虽然内容比较多,但都比较简单,详细的代码就不贴了,大家感兴趣的可以去看源码。至于圆点动画,也是一个自定义View,外层只要根据滑动距离换算下动画的百分比传进去,在里面画出需要的图形就行了,判断当百分比到0.5的时候画三个圆 :

    public class ExpendPoint extends View {
    
        float percent;
        float maxRadius = 15;
        float maxDist = 60;
        Paint mPaint;
    
        public ExpendPoint(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
             mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setColor(Color.GRAY);
        }
        
        public void setPercent(float percent) {
            this.percent = percent;
            invalidate();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            float centerX = getWidth() / 2;
            float centerY = getHeight() / 2;
            if (percent <= 0.5f) {只画一个圆
                mPaint.setAlpha(255);
                float radius = percent * 2 * maxRadius;
                canvas.drawCircle(centerX, centerY, radius, mPaint);
            } else {//画三个个圆
                float afterPercent = (percent - 0.5f) / 0.5f;
                float radius = maxRadius - maxRadius / 2 * afterPercent;
                canvas.drawCircle(centerX, centerY, radius, mPaint);
                canvas.drawCircle(centerX - afterPercent * maxDist, centerY, maxRadius / 2, mPaint);
                canvas.drawCircle(centerX + afterPercent * maxDist, centerY, maxRadius / 2, mPaint);
            }
        }
    }
    

    拓展:上拉栏

    有了上面的基础,实现上拉加载栏也是很简单的,逻辑基本相同,只是方向变了而已。我们来看下最终效果:

    readme2.gif

    最后贴下本项目github地址:

    Android仿新版微信的小程序下拉栏

    相关文章

      网友评论

      • 克拉丶:跟着老大认真处理触摸机制

      本文标题:Android仿新版微信的小程序下拉栏

      本文链接:https://www.haomeiwen.com/subject/jdirnxtx.html