目录
- 目录
- 概述
- 使用流程
- 使用AbsTouchEventHandle
- 使用TouchUtils
- 细节易错点
- 关于TouchUtilsIMoveEvent
- 关于TouchUtilsIScaleEvent
- TestCircleDraw圆形缩放
- TestRectangleDraw矩形缩放
- 圆形矩形缩放接口实现代码
- 绘制View的其它细节
- onDrawCanvas
- TouchUtils在AbsTouchEventHandle抽象方法中的使用
- 绘制操作注意
- 矩形源码
- 自定义VIEW绘制显示
- GitHub示例地址
概述
本文主要介绍之前提到的AbsTouchEventHandle
(自定义触摸事件处理类)及TouchUtils
(触摸事件辅助工具类)如何结合一起使用.
使用的目的或者说达到的结果是:
简单方便地完成界面元素的拖动与缩放
在整个过程中,界面元素的拖动与缩放工作完全交给以上两个类处理,而不需要做任何其它的操作.只需要完成的是相关的一些接口实现与回调而已.
使用流程
以下为两个类结合使用的简单流程,若不是特别理解可以略过,下面会一一详细说明.
- 创建一个专门用于绘制整个界面的类,
Draw
- 使
Draw
继承自AbsTouchEventHandle
,并实现重写其所有抽象方法(暂时不需要处理) - 创建
TouchUtils
的实例对象,TestRectangleDraw
实现TouchUtils
里的移动及缩放接口IMoveEvent
及IScaleEvent
- 将
TouchUtils
实例对象与其接口实现类对象绑定以方便回调
使用AbsTouchEventHandle
我们知道,界面元素最终的显示是以View
的形式绘制到屏幕上.而View
的绘制工作全部都是在onDraw()
方法里处理的.
但这里我们需要创建一个新的类去完成绘制工作而不是直接通过View
去完成工作.这是因为
AbsTouchEventHandle
本身是个抽象类,意味着它必须被继承才能使用.而自定义View必定是继承自系统类View
,所以这是不可能再继承另外一个类的
除此之外,使用全新的类专门处理绘制工作还有一个好处是: 绘制工作是独立的,而不会与View
本身的某些方法混淆在一起.这是一种类似组合的方式而不是嵌套的方式.
由于绘制类不继承自View
,则需要与显示此绘制界面的View
关联起来,所以此绘制需要提供一些方法以将绘制界面成功连接到View
上.
最后一个重要的点,
AbsTouchEventHandle
本质是实现了触摸事件接口View.OnTouchListener
处理的,千万不要忘了将View
的onTouchListener
事件设置为当前的绘制类~~~
public class TestRectangleDraw extends AbsTouchEventHandle{ private View mDrawView; public TestRectangleDraw(View drawView){ mDrawView=drawView; //必须将绘制View的触摸监听事件替换成当前的类 mDrawView.setOnTouchListener(this); } //忽略抽象方法的重写,后面使用 TouchUtils 时再处理}
以上为AbsTouchEventHandle
类的使用,当继承此类时可以直接处理View的单击事件,双击事件(拖动与缩放需要TouchUtils
辅助)
使用TouchUtils
使用TouchUtils
辅助工具类是为了方便处理拖动事件与缩放事件,一般的拖动事件及缩放事件都不需要再自定义,直接使用TouchUtils
及实现相关的接口即可.
首先,根据之前有关TouchUtils
的介绍文章,我们知道使用此工具类需要实现对应的接口,这是一个很重要的操作.
关于接口的实现并不需要一定是当前的绘制类,如果你喜欢完全可以使用一个新类去处理这些接口.但实际并没有这个必要,这里我们直接使用绘制类去实现这些接口
之后需要在绘制类中创建
TouchUtils
的实例对象,同时将接口实现对象与其绑定,以实现其拖动与缩放的有效性.
//为了方便查看,忽略之前继承的 AbsTouchEventHandle,只看接口的实现public class TestRectangleDraw implements TouchUtils.IMoveEvent, TouchUtils.IScaleEvent{ private TouchUtils mTouch; public TestRectangleDraw(View drawView){ mDrawView=drawView; //必须将绘制View的触摸监听事件替换成当前的类 mDrawView.setOnTouchListener(this); mTouch=new TouchUtils(); mTouch.setMoveEvent(this); mTouch.setScaleEvent(this); } //接口实现内容下面会详细解释,暂时忽略}
以上即为TouchUtils
的基本使用.下面是关于一些细节部分的处理.
细节易错点
特别提出的是,这个使用上应该不会很难,但是在TouchUtils.IScaleEvent
接口的实现上,很可能出错率会很高.这并不是此两个类的问题,而是在处理数据时可能会考虑不完善的问题(已经踩了几次坑了),下面会有两个实现类的对比.文章最后也会将两个类完整地给出.可以做比较查看.
关于TouchUtils.IMoveEvent
前面两部分的操作是必须,这个地方只讨论这个接口的具体实现.
这个接口是为了实现拖动操作的.接口本身不处理任何的拖动操作事件,只是为了
确认是否允许拖动操作;拖动操作的执行及拖动操作失误时的处理.
这个接口一共有四个方法:
//是否允许X轴方向的拖动boolean isCanMovedOnX(float moveDistanceX, float newOffsetX);//是否允许Y轴方向的拖动boolean isCanMovedOnY(float moveDistacneY, float newOffsetY);//拖动事件(基本上就是重绘操作)void onMove(int suggestEventAction);//拖动失败事件(不能拖动)void onMoveFail(int suggetEventAction);
接口的功能应该是很明确的.下面给出一个简单的实现.
//允许X轴方向的拖动boolean isCanMovedOnX(float moveDistanceX, float newOffsetX){ return true;}//允许Y轴方向的拖动boolean isCanMovedOnY(float moveDistacneY, float newOffsetY){ return true;}//通知View重绘void onMove(int suggestEventAction){ mDrawView.postInvalidate();}//拖动失败事件(不能拖动)void onMoveFail(int suggetEventAction){ //一般都不需要特别处理,可以选择提醒用户不能拖动了}
拖动事件接口的实现并不会有很大的麻烦,一般也不容易出错.唯一的问题是在isCanMovedOn
方法中根据实际的情况去确定什么时候可以进行拖动什么时候不可以(绘制元素边界?还是达到屏幕边界?)
关于TouchUtils.IScaleEvent
缩放回调事件会相对复杂一点.这是因为拖动的时候元素大小是不变的,也就是界面元素的本身的属性都是不变的.仅仅改变的是坐标.
但从关于TouchUtils
的文章中我们已经是将坐标的变化与元素本身的坐标是分开的.即不管在任何时候,元素都只处理初始化时的坐标,任何移动产生偏移的坐标都由TouchUtils
类去处理.
但是缩放是不一样的.缩放时意味着元素本身的属性会改变,大小,长宽等都会改变.包括自身的坐标.
由于绘制的元素是什么工具类无法确定,所以整个元素的变化操作只能交给绘制元素的类去处理.即工具类不负责缩放元素的属性变化,只会告诉绘制类变化的比例.
基于这个原则,TouchUtils.IScaleEvent
就有存在的需要了.同理,参考TouchUtils.IMoveEvent
的方法,IScaleEvent
也有四个方法
//是否允许缩放,缩放肯定是整个元素一起缩放的,不存在某个维度可以缩放而另外一个却不可以的情况boolean isCanScale(float newScaleRate);//设置元素缩放比void setScaleRate(float newScaleRate, boolean isNeedStoreValue);//缩放操作(基本上是重绘)void onScale(int suggestEventAction);//缩放失败操作(不能缩放的情况下)void onScaleFail(int suggetEventAction);
以上四个方法应该也不难理解的.其中onScale
及onScaleFail
两个方法是最容易的.isCanScale
取决于用户需求而是否复杂,在任何情况下都可以缩放直接返回true
即可.
最重要的一个方法,也可能是最复杂和容易出错的:setScaleRate
,必须记住的是
setScaleRate
是用于处理元素缩放属性数据的.在一次缩放事件中,每一次回调都是相对开始缩放前的原始元素大小的比例.
以下重点解释此方法,本小节最后会给出此接口实现的完整代码.
一次缩放事件是指: 两点触摸按下时到两点触摸抬起时整个过程
开始缩放前原始元素大小是指: 两点触摸按下时元素的大小
整个缩放过程的比例都是以 两点按下时元素的大小为基础的
在下面会使用两个不同的情景来解释此方法的使用.其中一个是TestCircleDraw
,界面元素仅为一个圆形;
另外一个是TestRectangleDraw
,界面元素仅为一个矩形;
通过这两个实际的不同的界面元素的绘制来说明该方法使用时需要注意的一些问题
使用缩放前,我们需要确定的是元素缩放到底是需要缩放的是什么?
TestCircleDraw
圆形缩放
圆形缩放,本质需要缩放的是: 半径
//全局定义的半径变量//绘制时使用的半径变量(包括缩放时半径不断变化也是使用此变量)float mRadius;//用于保存每一次缩放后的固定半径变量(仅在缩放后才会改变,其它时间不会改变)float mTempRadius;//实现方法public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) { //计算新的半径 mRadius = mTempRadius * newScaleRate; //当返回的标志为true时,提醒为已经到了up事件 //此时应该把最后一次缩放的比例当做最终的数据保存下来 if (isNeedStoreValue) { //缩放结束,保存状态 mTempRadius = mRadius; }}
这个地方应该不会很难理解.要记住~
mRadius
是绘制时使用的变量,在整个缩放过程会不断变化;而mTempRadius
是用于暂存变量,仅会在缩放后改变,其它时候是不变的
圆形的应该是比较容易理解,这也是放在前面先讲的原因,下面的是矩形,复杂度会提升一些.
TestRectangleDraw
矩形缩放
同理,先考虑需要缩放的是什么?
矩形需要缩放的必定是宽高,而不是坐标,这个很重要.
由于矩形绘制时是使用RectF
类来记录矩形的属性数据,此处我们也需要创建对应的变量来记录缩放前后的数据.
//定义全局变量//绘制时使用的变量(同理,会不断变化)RectF mRectf;//用于保存每一次缩放后的数据RectF mTempRectF;//接口方法实现public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) { //计算新的宽度 float newWidth = mTempRectf.width() * newScaleRate; //计算新的高度 float newHeight = mTempRectf.height() * newScaleRate; //计算中心位置 float centerX = mTempRectf.centerX(); float centerY = mTempRectf.centerY(); //根据中心位置调整大小 //此处确保了缩放时是按绘制物体中心为标准 mDrawRectf.left = centerX - newWidth / 2; mDrawRectf.top = centerY - newHeight / 2; mDrawRectf.right = mDrawRectf.left + newWidth; mDrawRectf.bottom = mDrawRectf.top + newHeight; //缩放事件结束,需要保存数据 if (isNeedStoreValue) { mTempRectf = new RectF(mDrawRectf); }}
可见,矩形的保存工作比圆形复杂得多,当然,实际的流程并不会很难理解.当需要处理的元素会比较复杂时,那么保存工作需要做的事情将会更多,这种时候很容易会出错,所以这个方法即是缩放的重点,也是容易出错的地方.
最后必须注意的是:
缩放肯定会存在一个缩放中心,需要确定元素到底是要中心缩放还是以某点为缩放中心.
如以上矩形,若更新矩形坐标时使用的是
mDrawRectf.right=mDrawRectf.left+newWidth;mDrawRectf.bottom=mDrawRectf.top+newHeight;
此时将以左上解为缩放中心,而不是元素物体的中心.
圆形/矩形缩放接口实现代码
圆形:
@Overridepublic boolean isCanScale(float newScaleRate) { return true;}@Overridepublic void setScaleRate(float newScaleRate, boolean isNeedStoreValue) { //更新当前的数据 //newScaleRate缩放比例一直是相对于按下时的界面的相对比例,所以在移动过程中 //每一次都是要与按下时的界面进行比例缩放,而不是针对上一次的结果 //使用这种方式一方面在缩放时的思路处理是比较清晰的 //另一方面是缩放的比例不会数据很小(若相对于上一次,每一次move移动几个像素, //这种情况下缩放的比例相对上一次肯定是0.0XXXX,数据量一小很容易出现一些不必要的问题) mRadius = mTempRadius * newScaleRate; //当返回的标志为true时,提醒为已经到了up事件 //此时应该把最后一次缩放的比例当做最终的数据保存下来 if (isNeedStoreValue) { mTempRadius = mRadius; }}@Overridepublic void onScale(int suggestEventAction) { mDrawView.postInvalidate();}@Overridepublic void onScaleFail(int suggetEventAction) {}
矩形
@Overridepublic boolean isCanScale(float newScaleRate) { return true;}@Overridepublic void setScaleRate(float newScaleRate, boolean isNeedStoreValue) { float newWidth = mTempRectf.width() * newScaleRate; float newHeight = mTempRectf.height() * newScaleRate; //计算中心位置 float centerX = mTempRectf.centerX(); float centerY = mTempRectf.centerY(); //根据中心位置调整大小 //此处确保了缩放时是按绘制物体中心为标准 mDrawRectf.left = centerX - newWidth / 2; mDrawRectf.top = centerY - newHeight / 2; mDrawRectf.right = mDrawRectf.left + newWidth; mDrawRectf.bottom = mDrawRectf.top + newHeight; //此方式缩放中心为左上角 //mDrawRectf.right=mDrawRectf.left+newWidt //mDrawRectf.bottom=mDrawRectf.top+newHeig if (isNeedStoreValue) { mTempRectf = new RectF(mDrawRectf); }}@Overridepublic void onScale(int suggestEventAction) { mDrawView.postInvalidate();}@Overridepublic void onScaleFail(int suggetEventAction) {}
绘制View的其它细节
onDraw(Canvas)
由以上得知,绘制界面时本质是View.onDraw()
方法,而绘制类只是我们创建的一个类,所以我们也提供了对应的方法提供给自定义View在onDraw()
方法中调用
//忽略继承类及接口等无关因素public class TestRectangleDraw{ public void onDraw(Canvas canvas){ //绘制操作 }}
TouchUtils
在AbsTouchEventHandle
抽象方法中的使用
TouchUtils
类中有对应的方法处理拖动及缩放事件,直接在抽象方法中调用即可;至于单击和双击事件不由TouchUtils
处理.
@Overridepublic void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent) { //工具类默认处理的单点触摸事件 mTouch.singleTouchEvent(event, extraMotionEvent);}@Overridepublic void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent) { //工具类默认处理的多点(实际只处理了两点事件)触摸事件 mTouch.multiTouchEvent(event, extraMotionEvent);}@Overridepublic void onSingleClickByTime(MotionEvent event) { //基于时间的单击事件 //按下与抬起时间不超过500ms}@Overridepublic void onSingleClickByDistance(MotionEvent event) { //基于距离的单击事件 //按下与抬起的距离不超过20像素(与时间无关,若按下不动几小时后再放开只要距离在范围内都可以触发)}
绘制操作注意
在绘制元素时,需要注意的是,TouchUtils
处理了所有的与移动相关的偏移量,并保存到TouchUtils
中.所以绘制元素时需要将该偏移量使用上才可以真正显示出移动后的界面.
以矩形绘制为例
//正常绘制矩形//canvas.drawRect(mDrawRectf.left, mDrawRectf.top,mDrawRectf.right,mDrawRectf.bottom,mPaint);//正确绘制矩形float offsetX=mTouchUtils.getOffsetX();float offsetY=mTouchUtils.getOffsetY();//必须将offset偏移量部分添加到绘制的实际坐标中才可以正确绘制移动后的元素canvas.drawRect(mDrawRectf.left + offsetX, mDrawRectf.top + offsetY, mDrawRectf.right + offsetX, mDrawRectf.bottom + offsetY, mPaint);
矩形源码
//矩形绘制类public class TestRectangleDraw extends AbsTouchEventHandle implements TouchUtils.IMoveEvent, TouchUtils.IScaleEvent { //创建工具 private TouchUtils mTouch = null; //保存显示的View private View mDrawView = null; //画笔 private Paint mPaint = null; //绘制时使用的数据 private RectF mDrawRectf = null; //缩放时保存的缩放数据 //此数据保存的是每一次缩放后的数据(屏幕不存在触摸时,才算缩放后,缩放时为滑动屏幕期间) private RectF mTempRectf = null; public TestRectangleDraw(View drawView) { mTouch = new TouchUtils(); mTouch.setMoveEvent(this); mTouch.setScaleEvent(this); mDrawView = drawView; mDrawView.setOnTouchListener(this); mPaint = new Paint(); mPaint.setAntiAlias(true); //起始位置为 300,300 //宽为200,长为300 mDrawRectf = new RectF(); mDrawRectf.left = 300; mDrawRectf.right = 500; mDrawRectf.top = 300; mDrawRectf.bottom = 600; //必须暂存初始化时使用的数据 mTempRectf = new RectF(mDrawRectf); mTouch.setIsShowLog(false); this.setIsShowLog(false, null); } //回滚移动位置 public void rollback() { mTouch.rollbackToLastOffset(); } public void onDraw(Canvas canvas) { mPaint.setColor(Color.BLACK); mPaint.setStyle(Paint.Style.FILL); //此处是实际的绘制界面+偏移量,偏移量切记不能保存到实际绘制的数据中!!!! //不可以使用 mDrawRectf.offset(x,y) canvas.drawRect(mDrawRectf.left + mTouch.getDrawOffsetX(), mDrawRectf.top + mTouch.getDrawOffsetY(), mDrawRectf.right + mTouch.getDrawOffsetX(), mDrawRectf.bottom + mTouch.getDrawOffsetY(), mPaint); } @Override public void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent) { //工具类默认处理的单点触摸事件 mTouch.singleTouchEvent(event, extraMotionEvent); } @Override public void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent) { //工具类默认处理的多点(实际只处理了两点事件)触摸事件 mTouch.multiTouchEvent(event, extraMotionEvent); } @Override public void onSingleClickByTime(MotionEvent event) { //基于时间的单击事件 //按下与抬起时间不超过500ms } @Override public void onSingleClickByDistance(MotionEvent event) { //基于距离的单击事件 //按下与抬起的距离不超过20像素(与时间无关,若按下不动几小时后再放开只要距离在范围内都可以触发) } @Override public void onDoubleClickByTime() { //基于时间的双击事件 //单击事件基于clickByTime的两次单击 //两次单击之间的时间不超过250ms } @Override public boolean isCanMovedOnX(float moveDistanceX, float newOffsetX) { return true; } @Override public boolean isCanMovedOnY(float moveDistacneY, float newOffsetY) { return true; } @Override public void onMove(int suggestEventAction) { mDrawView.postInvalidate(); } @Override public void onMoveFail(int suggetEventAction) { } @Override public boolean isCanScale(float newScaleRate) { return true; } @Override public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) { float newWidth = mTempRectf.width() * newScaleRate; float newHeight = mTempRectf.height() * newScaleRate; //计算中心位置 float centerX = mTempRectf.centerX(); float centerY = mTempRectf.centerY(); //根据中心位置调整大小 //此处确保了缩放时是按绘制物体中心为标准 mDrawRectf.left = centerX - newWidth / 2; mDrawRectf.top = centerY - newHeight / 2; mDrawRectf.right = mDrawRectf.left + newWidth; mDrawRectf.bottom = mDrawRectf.top + newHeight; //此方式缩放中心为左上角// mDrawRectf.right=mDrawRectf.left+newWidth;// mDrawRectf.bottom=mDrawRectf.top+newHeight; if (isNeedStoreValue) { mTempRectf = new RectF(mDrawRectf); } } @Override public void onScale(int suggestEventAction) { mDrawView.postInvalidate(); } @Override public void onScaleFail(int suggetEventAction) { }}
自定义VIEW绘制显示
/** * Created by CT on 15/9/25. * 此View演示了AbsTouchEventHandle与TouchUtils怎么用 */public class TestView extends View { //创建绘制圆形示例界面专用的绘制类 TestCircleDraw mTestCircleDraw = new TestCircleDraw(this, getContext()); //创建绘制方形示例界面专用的绘制类 TestRectangleDraw mTestRectfDraw = new TestRectangleDraw(this); public TestView(Context context) { super(context); } public TestView(Context context, AttributeSet attrs) { super(context, attrs); } public TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } //回滚移动位置 public void rollback() { mTestRectfDraw.rollback(); } @Override protected void onDraw(Canvas canvas) { //实际上的绘制工作全部都交给了绘制专用类 //在绘制很复杂的界面时,这样可以很清楚地分开 //绘制与视图,因为视图本身可能还要处理其它的事件(比如来自绘制事件中回调的事件等) //而且View本身的方法就够多了,还加一很多绘制方法,看起来也不容易理解// mTestCircleDraw.onDraw(canvas); mTestRectfDraw.onDraw(canvas); }}
GitHub示例地址
https://github.com/CrazyTaro/TouchEventHandle
回到目录