Android仿新版微信的小程序下拉栏
上周微信更新到了6.6.1版本,加入了微信小游戏。朋友圈都在玩跳一跳。而且现在微信把最近用过的小程序放到了首页顶部,轻轻下拉就可以快速访问了。可以看下效果,如果还没升级的朋友可要抓紧了。
自己作为一个安卓程序员,虽然不会写小程序,但也要紧跟热潮(蹭热点)啊。于是乎就干脆仿写下这个下拉控件吧。第七宇宙惯例,先上效果图:
上图的主界面是我上一篇文章《撸一款全手势操作浏览器》写的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,这是为了防止快速拖动引发刷新 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)
方法将布局整体滚动。
遇到的一个问题
我在上面这样写好之后,跑了一遍,发现 onInterceptTouchEvent
只执行了ACTION_DOWN,ACTION_MOVE事件不会执行,也就无法拦截和进行滑动了。百度下原来有这么一个规则:
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,原点的TranslationY和Alpha就可以了。实现起来也是比较简单的。详细的代码就不贴了,大家感兴趣的可以去看源码。至于原点动画,也是一个自定义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); } } }
最后贴下本项目github地址:Android仿新版微信的小程序下拉栏