2019独角兽企业重金招聘Python工程师标准>>>

昨天完成了一个支持设置margin,gravity,水平或者垂直排列的简单的自定义ViewGroup。但是它并不支持滑动,所以无法展现较多的内容。现在我们重写一下onTouchEvent(),来支持滑动。

重写onTouchEvent()以支持滑动:

要使View滑动,我们可以通过调用scrollTo()和scrollBy()来实现,这里需要注意的是:要使页面向左移动,需要增加mScrollX(就是向scrollBy传递一个正数),同样的,要使页面向上移动,需要增加mScrollY。

@Override
public boolean onTouchEvent(MotionEvent event) {final int action = event.getAction();if (BuildConfig.DEBUG)Log.d("onTouchEvent", "action: " + action);switch (action) {case MotionEvent.ACTION_DOWN:x = event.getX();y = event.getY();break;case MotionEvent.ACTION_MOVE:float mx = event.getX();float my = event.getY();//此处的moveBy是根据水平或是垂直排放的方向,//来选择是水平移动还是垂直移动moveBy((int) (x - mx), (int) (y - my));x = mx;y = my;break;}return true;
}//此处的moveBy是根据水平或是垂直排放的方向,
//来选择是水平移动还是垂直移动
public void moveBy(int deltaX, int deltaY) {if (BuildConfig.DEBUG)Log.d("moveBy", "deltaX: " + deltaX + "    deltaY: " + deltaY);if (orientation == Orientation.HORIZONTAL) {if (Math.abs(deltaX) >= Math.abs(deltaY))scrollBy(deltaX, 0);} else {if (Math.abs(deltaY) >= Math.abs(deltaX))scrollBy(0, deltaY);}
}

好,现在我们再运行这段代码,就会发现View已经可以跟随手指移动了,但现在的问题是当手指离开屏幕后,View就立即停止滑动了,这样的体验就相当不友好,那么我们希望手指离开后,View能够以一定的阻尼满满地减速滑动。

借助Scroller,并且处理ACTION_UP事件

Scroller是一个用于计算位置的工具类,它负责计算下一个位置的坐标(根据时长,最小以最大移动距离,以及阻尼算法(可以使用自定义的Interpolator))。

Scroller有两种模式:scroll和fling。

  1. scroll用于已知目标位置的情况(例如:Viewpager中向左滑动,就是要展示右边的一页,那么我们就可以准确计算出滑动的目标位置,此时就可以使用Scroller.startScroll()方法)
  2. fling用于不能准确得知目标位置的情况(例如:ListView,每一次的滑动,我们事先都不知道滑动距离,而是根据手指抬起是的速度来判断是滑远一点还是近一点,这时就可以使用Scroller.fling()方法)

现在我们改一下上面的onTouchEvent()方法,增加对ACTION_UP事件的处理,以及初速度的计算。

@Override
public boolean onTouchEvent(MotionEvent event) {final int action = event.getAction();if (BuildConfig.DEBUG)Log.d("onTouchEvent", "action: " + action);//将事件加入到VelocityTracker中,用于计算手指抬起时的初速度if (velocityTracker == null) {velocityTracker = VelocityTracker.obtain();}velocityTracker.addMovement(event);switch (action) {case MotionEvent.ACTION_DOWN:x = event.getX();y = event.getY();if (!mScroller.isFinished())mScroller.abortAnimation();break;case MotionEvent.ACTION_MOVE:float mx = event.getX();float my = event.getY();moveBy((int) (x - mx), (int) (y - my));x = mx;y = my;break;case MotionEvent.ACTION_UP://maxFlingVelocity是通过ViewConfiguration来获取的初速度的上限//这个值可能会因为屏幕的不同而不同velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);float velocityX = velocityTracker.getXVelocity();float velocityY = velocityTracker.getYVelocity();//用来处理实际的移动completeMove(-velocityX, -velocityY);if (velocityTracker != null) {velocityTracker.recycle();velocityTracker = null;}break;return true;
}

我们在computeMove()中调用Scroller的fling()方法,顺便考虑一下滑动方向问题

private void completeMove(float velocityX, float velocityY) {if (orientation == Orientation.HORIZONTAL) {int mScrollX = getScrollX();int maxX = desireWidth - getWidth();// - Math.abs(mScrollX);if (Math.abs(velocityX) >= minFlingVelocity && maxX > 0) {mScroller.fling(mScrollX, 0, (int) velocityX, 0, 0, maxX, 0, 0);invalidate();}} else {int mScrollY = getScrollY();int maxY = desireHeight - getHeight();// - Math.abs(mScrollY);if (Math.abs(velocityY) >= minFlingVelocity && maxY > 0) {mScroller.fling(0, mScrollY, 0, (int) velocityY, 0, 0, 0, maxY);invalidate();}}
}

好了,现在我们再运行一遍,问题又来了,手指抬起后,页面立刻又停了下来,并没有实现慢慢减速的滑动效果。

其实原因就是上面所说的,Scroller只是帮助我们计算位置的,并不处理View的滑动。我们要想实现连续的滑动效果,那就要在View绘制完成后,再通过Scroller获得新位置,然后再重绘,如此反复,直至停止。

重写computeScroll(),实现View的连续绘制

@Override
public void computeScroll() {if (mScroller.computeScrollOffset()) {if (orientation == Orientation.HORIZONTAL) {scrollTo(mScroller.getCurrX(), 0);postInvalidate();} else {scrollTo(0, mScroller.getCurrY());postInvalidate();}}
}

computeScroll()是在ViewGroup的drawChild()中调用的,上面的代码中,我们通过调用computeScrollOffset()来判断滑动是否已停止,如果没有,那么我们可以通过getCurrX()和getCurrY()来获得新位置,然后通过调用scrollTo()来实现滑动,这里需要注意的是postInvalidate()的调用,它会将重绘的这个Event加入UI线程的消息队列,等scrollTo()执行完成后,就会处理这个事件,然后再次调用ViewGroup的draw()-->drawChild()-->computeScroll()-->scrollTo()如此就实现了连续绘制的效果。

现在我们再重新运行一下app,终于可以持续滑动了:),不过,当我们缓慢地拖动View,慢慢抬起手指,我们会发现通过这样的方式,可以使得所有的子View滑到屏幕之外,(所有的子View都消失了:()。

问题主要是出在completeMove()中,我们只是判断了初始速度是否大于最小阈值,如果小于这个最小阈值的话就什么都不做,缺少了边界的判断,因此修改computeMove()如下:

private void completeMove(float velocityX, float velocityY) {if (orientation == Orientation.HORIZONTAL) {int mScrollX = getScrollX();int maxX = desireWidth - getWidth();if (mScrollX > maxX) {// 超出了右边界,弹回mScroller.startScroll(mScrollX, 0, maxX - mScrollX, 0);invalidate();} else if (mScrollX < 0) {// 超出了左边界,弹回mScroller.startScroll(mScrollX, 0, -mScrollX, 0);invalidate();} else if (Math.abs(velocityX) >= minFlingVelocity && maxX > 0) {mScroller.fling(mScrollX, 0, (int) velocityX, 0, 0, maxX, 0, 0);invalidate();}} else {int mScrollY = getScrollY();int maxY = desireHeight - getHeight();if (mScrollY > maxY) {// 超出了下边界,弹回mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY);invalidate();} else if (mScrollY < 0) {// 超出了上边界,弹回mScroller.startScroll(0, mScrollY, 0, -mScrollY);invalidate();} else if (Math.abs(velocityY) >= minFlingVelocity && maxY > 0) {mScroller.fling(0, mScrollY, 0, (int) velocityY, 0, 0, 0, maxY);invalidate();}}
}

ok,现在当我们滑出边界,松手后,会自动弹回。

处理ACTION_POINTER_UP事件,解决多指交替滑动跳动的问题

现在ViewGroup可以灵活的滑动了,但是当我们使用多个指头交替滑动时,就会产生跳动的现象。原因是这样的:

我们实现onTouchEvent()的时候,是通过event.getX(),以及event.getY()来获取触摸坐标的,实际上是获取的手指索引为0的位置坐标,当我们放上第二个手指后,这第二个手指的索引为1,此时我们同时滑动这两个手指,会发现没有问题,因为我们追踪的是手指索引为0的手指位置。但是当我们抬起第一个手指后,问题就出现了, 因为这个时候原本索引为1的第二个手指的索引变为了0,所以我们追踪的轨迹就出现了错误。

简单来说,跳动就是因为追踪的手指的改变,而这两个手指之间原本存在间隙,而这个间隙的距离就是我们跳动的距离。

其实问题产生的根本原因就是手指的索引会变化,因此我们需要记录被追踪手指的id,然后当有手指离开屏幕时,判断离开的手指是否是我们正在追踪的手指:

  1. 如果不是,忽略
  2. 如果是,则选择一个新的手指作为被追踪手指,并且调整位置记录。

还有一点就是,要处理ACTION_POINTER_UP事件,就需要给action与上一个掩码:event.getAction()&MotionEvent.ACTION_MASK 或者使用 event.getActionMasked()方法。

更改后的onTouchEvent()的实现如下:

@Override
public boolean onTouchEvent(MotionEvent event) {final int action = event.getActionMasked();if (velocityTracker == null) {velocityTracker = VelocityTracker.obtain();}velocityTracker.addMovement(event);switch (action) {case MotionEvent.ACTION_DOWN:// 获取索引为0的手指idmPointerId = event.getPointerId(0);x = event.getX();y = event.getY();if (!mScroller.isFinished())mScroller.abortAnimation();break;case MotionEvent.ACTION_MOVE:// 获取当前手指id所对应的索引,虽然在ACTION_DOWN的时候,我们默认选取索引为0// 的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指// 屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹,// 因此此处不能使用event.getActionIndex()来获得索引final int pointerIndex = event.findPointerIndex(mPointerId);float mx = event.getX(pointerIndex);float my = event.getY(pointerIndex);moveBy((int) (x - mx), (int) (y - my));x = mx;y = my;break;case MotionEvent.ACTION_UP:velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);float velocityX = velocityTracker.getXVelocity(mPointerId);float velocityY = velocityTracker.getYVelocity(mPointerId);completeMove(-velocityX, -velocityY);if (velocityTracker != null) {velocityTracker.recycle();velocityTracker = null;}break;case MotionEvent.ACTION_POINTER_UP:// 获取离开屏幕的手指的索引int pointerIndexLeave = event.getActionIndex();int pointerIdLeave = event.getPointerId(pointerIndexLeave);if (mPointerId == pointerIdLeave) {// 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTrackerint reIndex = pointerIndexLeave == 0 ? 1 : 0;mPointerId = event.getPointerId(reIndex);// 调整触摸位置,防止出现跳动x = event.getX(reIndex);y = event.getY(reIndex);if (velocityTracker != null)velocityTracker.clear();}break;}return true;
}

好了,现在我们用多个手指交替滑动就很正常了。

不过当我们想为咱们的自定义的ViewGroup设置onClick和onLongClick事件时,发现并不支持。更奇怪的是当我们为子View设置了事件之后(例如click事件),我们的ViewGroup居然不能正常滑动了。

上面第一个问题,我们需要在ACTION_UP中加一些处理,而第二个问题就需要重写onInterceptTouchEvent()方法,关于onInterceptTouchEvent()和onTouchEvent()之间的事件传递流程,就在明天的博客中再写吧:)

转载于:https://my.oschina.net/fengheju/blog/196455

自定义ViewGroup (2)支持滑动,并处理多指触摸可能产生的跳动问题相关推荐

  1. android layout_margin的值,Android自定义ViewGroup( 支持layout_margin属性)

    3. 支持layout_margin属性 如果我们自定义的布局参数类继承自MarginLayoutParams,就自动支持了layout_margin属性了,我们需要做的就是直接在布局文件中使用lay ...

  2. android 自定义ViewGroup和对view进行切图动画实现滑动菜单SlidingMenu[转]

    http://blog.csdn.net/jj120522/article/details/8095852 示意图就不展示了,和上一节的一样,滑动菜单SlidingMenu效果如何大家都比较熟悉,在这 ...

  3. 自定义ViewGroup (3) 与子View之间 Touch Event的拦截与处理

    2019独角兽企业重金招聘Python工程师标准>>> 在昨天的博客(自定义ViewGroup(2))中,我们解决了多个手指交替滑动带来的页面的跳动问题.但同时也还遗留了两个问题. ...

  4. Android自定义ViewGroup第十二式之年年有鱼

    前言 先来看两张效果图: 哈哈,就是这样了. 前段时间在鸿神的群里看到有群友截了一张QQ空间的图,问它那个是怎么实现的: 在好友动态的列表中多了个Header,这个Header有一叠卡片的效果,上面的 ...

  5. android 自定义图片容器,Android应用开发中自定义ViewGroup视图容器的教程

    一.概述在写代码之前,我必须得问几个问题: 1.ViewGroup的职责是啥?ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性, ...

  6. Android学习:自定义ViewGroup方法总结

    毕设应用中需要添加一个滑动按钮,在网上看了几个Demo之后决定自定义ViewGroup来实现. 这里是对实现过程中自定义ViewGroup的方法总结. 关于ViewGroup,文档给出的描述是: A ...

  7. Android 手把手教您自定义ViewGroup

    最近由于工作的变动,导致的博客的更新计划有点被打乱,希望可以尽快脉动回来~ 今天给大家带来一篇自定义ViewGroup的教程,说白了,就是教大家如何自定义ViewGroup,如果你对自定义ViewGr ...

  8. Android自定义ViewGroup的OnMeasure和onLayout详解

    前一篇文章主要讲了自定义View为什么要重载onMeasure()方法http://blog.csdn.net/tuke_tuke/article/details/73302595 那么,自定义Vie ...

  9. android滚动条布局横向,Android自定义ViewGroup实现可滚动的横向布局(2)

    这里直接代码: package com.example.libingyuan.horizontallistview.ScrollViewGroup; import android.content.Co ...

最新文章

  1. STL六大组件:分配器、容器、迭代器、算法、仿函数、适配器
  2. View类的xml属性和相关方法说明
  3. Mallet Java【Windows下配置】(解决Ant安装可能会失败的解决方案)
  4. Debian 项目不再提供 CD 格式的 ISO 镜像
  5. MVC把表格导出到Excel
  6. WebRTC Audio Encoder/Decoder Factory 的实现
  7. 主管护士需要考计算机和英语吗,2020主管护师改为机考,一定要注意这些问题!...
  8. 武侠乂怎么修改服务器,武侠乂怎么操作 按键功能详细介绍
  9. 耐思尼克域名注册:通过icann之后和之前的那些小故事
  10. 搭建Web站点和FTP站点
  11. atitit.web 推送实现方案集合
  12. 易语言输入框参数和调用
  13. 华为太极magisk安装教程_教程:如何升级太极内部的应用
  14. 自抗扰控制器-6线性自抗扰控制器LADRC
  15. 聚焦“共同富裕”,盛世昊通主题会议落实履行社会责任的政策
  16. 人生就像微信,迭代才有机会
  17. JavaScript设计模式与开发实践(网课学习)
  18. 2012河北省职称计算机题,2012河北省职称计算机考试模拟练习题1
  19. 如何把托管的网站放到服务器,web-server – 在家中托管网站,ftp和随机使用的服务器?...
  20. ZZULIOJ:1151: 大整数加法

热门文章

  1. windows下如何使用QT编写dll程序 .
  2. 机器视觉——单目相机模型(坐标标定以及去畸变)
  3. 详说sizeof与strlen的区别与联系
  4. mysql 注册驱动_mysql8.0以上版本注册驱动并建立数据库的连接公共代码
  5. qt 编译成apk_GitHub - qtxtz/AndroidMerageAPK: 实现android自动打包的程序
  6. 数学实验matlab课后习题,数学实验练习题(MATLAB)
  7. 凑零钱动态规划java_动态规划巧解凑零钱问题 | 创作者训练营
  8. 热力图怎么做_LncRNA这么热,5分左右的LncRNA研究文章应该怎么做
  9. mysql update 几万 非常慢_Mysql优化专题
  10. 参数化测试 junit_JUnit参数化测试