上周微信更新到了 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(MotionEventevent)
- boolean onTouchEvent(MotionEvent ev)
- @Override
- public
- final
- boolean
- onInterceptTouchEvent
- (MotionEventevent)
- {
- finalint action =event.getAction();
- //不拦截
- if(action ==MotionEvent.ACTION_CANCEL || action ==MotionEvent.ACTION_UP){
- mIsHandledTouchEvent =false;
- returnfalse;
- }
- //如果不是重新开始触摸且已经判断需要拦截,就一直拦截整套触摸事件
- if(action !=MotionEvent.ACTION_DOWN && mIsHandledTouchEvent){
- returntrue;
- }
- switch(action){
- caseMotionEvent.ACTION_DOWN:
- mLastMotionY =event.getY();
- mIsHandledTouchEvent =false;
- break;
- caseMotionEvent.ACTION_MOVE:
- finalfloat deltaY =event.getY()- mLastMotionY;
- finalfloat 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 是滑动阻尼值。
- @Overridepublicfinalboolean onTouchEvent(MotionEvent ev){
- boolean handled =false;
- switch(ev.getAction()){
- caseMotionEvent.ACTION_DOWN:
- mLastMotionY = ev.getY();
- mIsHandledTouchEvent =false;
- break;
- caseMotionEvent.ACTION_MOVE:
- finalfloat deltaY = ev.getY()- mLastMotionY;
- mLastMotionY = ev.getY();
- if(isPullRefreshEnabled()&& isReadyForPullDown()){
- pullHeaderLayout(deltaY / offsetRadio);
- handled =true;
- }else{
- mIsHandledTouchEvent =false;
- }
- break;
- caseMotionEvent.ACTION_CANCEL:
- caseMotionEvent.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) 方法将布局整体滚动。
我在上面这样写好之后,跑了一遍,发现
只执行了 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 方法处理。
- onInterceptTouchEvent
因为目前的子 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;
- publicExpendPoint(Context context,@NullableAttributeSet attrs){
- super(context, attrs);
- mPaint =newPaint();
- 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 仿新版微信的小程序下拉栏
来源: https://juejin.im/post/5a4c90c15188257c4d1b8d0c