Android定制视图及手势检测的基本示例
Android允许开发人员自定义视图,以实现特殊的效果。
自定义视图的步骤非常简单,基本上可以分为两步:
1、自定以类,继承合适的父类。
对于不包含子视图的类,一般直接继承自View;
包含子视图的类,可以继承FrameLayout等。
2、覆盖父类中的构造函数及回调接口。
自定义视图一般至少覆盖一个父类的构造函数,
并选择性地覆盖其它回调接口,以定制视图行为。
本篇博客就以一个简单的示例,记录一下Android中定制视图的方法。
1、定义类
首先定义出定制视图对应类,如下面代码所示:
..............public class BoxDrawingView extends View { ........... //实现该构造函数后,才能在xml中直接使用该定制View //xml中的属性信息,将通过AttributeSet传入到该接口 public BoxDrawingView(Context context, AttributeSet attrs) { super(context, attrs); ............... } ...........}
有了该类后,就可以在布局文件中直接使用了,类似于:
<stark.a.is.zhang.draganddraw.view.BoxDrawingView xmlns:android="http://schemas.android.com/apk/res/android" --定义id后,才能利用View的onSaveInstanceState、onRestoreInstanceState(Parcelable state) --> android:id="@+id/boxDrawingView" android:layout_width="match_parent" android:layout_height="match_parent"/>
在使用自定义视图时,必须使用类的全路径类名,这样布局inflater才能找到它。
布局inflater解析布局XML文件,并按视图定义创建View实例。
如果元素名不是全路径类名,布局inflater会转而在android.view和android.widget包中寻找目标。
如果目标视图类放置在其它包中,但未使用全路径,布局inflater将无法找到目标并最终导致应用崩溃。
2、覆盖父类函数
自定义视图可以覆盖父类的函数,实现自己的功能。
例如,可以覆盖onTouchEvent来捕捉屏幕触摸事件,
以下代码中列举了比较常用的事件:
........... @Override public boolean onTouchEvent(MotionEvent event) { //PointF为容器类,可以用于保存当前触摸点的位置坐标信息 PointF current = new PointF(event.getX(), event.getY()); switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: //捕获到单指按下,或者说捕获到第一个指头按下 ........................ break; case MotionEvent.ACTION_MOVE: //捕获到指头移动,多个指头触摸屏幕时,任意指头移动均会触发 ........................ break; case MotionEvent.ACTION_UP: //捕获到最后一个指头离开,这个事件触发后,屏幕上应该没有指头了 ............... break; case MotionEvent.ACTION_POINTER_DOWN: //在有一个指头按下的前提下,捕获到其它指头按下 ............ break; case MotionEvent.ACTION_POINTER_UP: //屏幕上有多个指头时,有一个指头离开屏幕时触发 //即多个指头时,最后一个离开触发ACTION_UP,其余离开均是触发ACTION_POINTER_UP ........... break; case MotionEvent.ACTION_CANCEL: //触摸屏幕的事件,被当前视图的父视图拦截 ............... break; } return true; } ............
此外,一般还可以覆盖View的onDraw方法,实现视图的自定义绘制。
@Overrideprotected void onDraw(Canvas canvas) { ............}
应用启动时,所有视图都处于无效状态。
为了绘制视图,Android会调用顶级View视图的draw方法,引起自上而下的链式调用。
即顶级视图完成自我绘制,然后完成是其子视图的自我绘制,再然后是子视图的子视图的自我绘制。
如此调用下去直至继承结构的末端。
当继承结构中的所有视图都完成自我绘制后,最顶级View视图也就生效了。
在代码中主动调用View的invalidate接口,可以强制令View调用onDraw重新绘制自己。
三、设备旋转的问题
设备旋转后,视图会进行重绘,如果需要保存视图绘制相关的状态,则需要覆盖以下方法:
@Overrideprotected Parcelable onSaveInstanceState() { ..........}@Overrideprotected void onRestoreInstanceState(Parcelable state) { ........}
上文已经提到过,自定义View有id时,才能利用onSaveInstanceState、onRestoreInstanceState(Parcelable state)
保存和回复视图相关的状态信息。
而且与Fragment、Activity不同的是,View保存和回复使用的是基于Parcelable接口的对象。
一般我们可以按照Parcelable的接口规则,定义实现该接口的对象;
也可以使Bundle封装需要保存的信息,然后保存Bundle。
因为Bundle本身就是个继承了Parcelable接口的对象。
需要注意的是,在自定义视图中保存信息时,还需要保存父视图相关的信息。
整个过程的示例代码类似于:
................. @Override protected Parcelable onSaveInstanceState() { Bundle state = new Bundle(); //保存父类状态 state.putParcelable(PARENT_KEY, super.onSaveInstanceState()); ArrayList boxInfo = new ArrayList<>(); //mBoxes中保存的是视图中的一些信息 for (Box box : mBoxes) { Bundle bundle = new Bundle(); bundle.putParcelable(ORIGIN_KEY, box.getOrigin()); bundle.putParcelable(CURRENT_KEY, box.getCurrent()); bundle.putFloat(DEGREE_KEY, box.getDegree()); boxInfo.add(bundle); } state.putParcelableArrayList(BOX_INFO_KEY, boxInfo); return state; } @Override protected void onRestoreInstanceState(Parcelable state) { Bundle bundle = (Bundle) state; //恢复父视图中的信息 super.onRestoreInstanceState((Parcelable) bundle.get(PARENT_KEY)); ArrayList boxInfo = bundle.getParcelableArrayList(BOX_INFO_KEY); if (boxInfo != null && boxInfo.size() > 0) { for (Bundle info : boxInfo) { Box box = new Box((PointF) info.getParcelable(ORIGIN_KEY), (PointF) info.getParcelable(CURRENT_KEY)); box.setDegree(info.getFloat(DEGREE_KEY, 0)); //恢复Box中的信息 mBoxes.add(box); } } } .............
View的onRestoreInstanceState接口先于onDraw接口被调用,
即设备旋转后,先恢复保存的信息,然后才会进行绘制。
4、手势检测初探
要检测手势,首先需要识别出不同的手指。
在一次触摸屏幕的过程中,Android为每个手指分配了一个唯一的pointer ID。
根据pointer Id就可以知道每个手指在移动中的位置,从而计算出手势。
举例如下:
public class BoxDrawingView extends View { ........... //我们定义两种模式,分别为普通绘制模式,一种是旋转屏幕的模式 //一个手指触摸屏幕时,mNormalPointerId记录其pointer ID private static int NORMAL = 0; private int mNormalPointerId = -1; //第二个手指触摸屏幕时,mRotatePointerId记录其pointer ID private static int ROTATE = 1; private int mRotatePointerId = -1; //默认为NORMAL private int mMode = NORMAL; ........... @Override public boolean onTouchEvent(MotionEvent event) { ............ switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: ............. //第一个手指触摸屏幕时,触发MotionEvent.ACTION_DOWN信息 //此时利用MotionEvent的getPointerId接口,就可以得到手指对应的pointer ID //getPointerId的接口需要传入pointerIndex,利用getActionIndex接口可得到 mNormalPointerId = event.getPointerId(event.getActionIndex()); break; ........... case MotionEvent.ACTION_POINTER_DOWN: //在有一个手指触屏的情况下,有其它手指触摸屏幕,则触发ACTION_POINTER_DOWN消息 changeMode(event, false); break; case MotionEvent.ACTION_POINTER_UP: //多个手指触屏的情况下,非最后一个手指离开屏幕,触发ACTION_POINTER_UP消息 changeMode(event, true); break; } ................. } private void changeMode(MotionEvent event, boolean up) { //利用MotionEvent的getPointerCount接口 //可以得到该MotionEvent触发时,屏幕上手指的数量 int pointerCount = event.getPointerCount(); //因此手指离开后,屏幕上剩余手指的数量应减1 if (up) { --pointerCount; } //手指已经离开屏幕,不使用对应的pointer ID if (pointerCount == 2 && !up) { mMode = ROTATE; mRotatePointerId = event.getPointerId(event.getActionIndex()); } else if (pointerCount == 1){ mMode = NORMAL; mRotatePointerId = -1; .............. } }}
得到手指的pointer ID后,就可以得到每个手指的坐标了,例如:
@Overridepublic boolean onTouchEvent(MotionEvent event) { .............. switch (event.getAction() & MotionEvent.ACTION_MASK) { ........... case MotionEvent.ACTION_MOVE: if (mMode == NORMAL) { ........ } else if (mMode == ROTATE) { if (mRotatePointerId != -1 && mNormalPointerId != -1) { //利用MotionEvent的getX、getY接口,以及pointer ID //就可以得到每个手指的横纵坐标 float normalX = event.getX(mNormalPointerId); float normalY = event.getY(mNormalPointerId); float rotateX = event.getX(mRotatePointerId); float rotateY = event.getY(mRotatePointerId); //利用横纵坐标计算旋转角 float k = Math.abs((normalX-rotateX)/(normalY-rotateY)); float degree = (float) Math.toDegrees(Math.atan(k)); //P.S.: //canvas.rotate接口,大于0的度数,则顺时针转动 //小于0,则逆时针转动 //canvas的save和restore必须配套使用 if ((normalX < rotateX && normalY < rotateY) || (normalX > rotateX && normalY > rotateY)){ degree = -degree; } ................ } break; } ..........}
自己做了下测试,例如当有A、B、C、D四个手指放到屏幕上时,
将依次得到4个不同的pointer ID,一般是0、1、2和3。
将手指B离开屏幕,重新发上B或E手指,分配的仍是序号1。
按照我上面写的代码,手指A对应的将是normalPointer ID,手指B得到的将是rotatePointer ID。
C、D的pointer ID并未保存。
此时,保持屏幕上有四个手指,然后做出旋转的滑动,旋转角度将以A、B的位置为准。
现在,我让A、B手指离开屏幕,用C、D手指做旋转动作,发现屏幕将以C、D的位置计算旋转角度。
由此,可以得到:
每个手指都会分配一个确定的pointer ID,
但MotionEvent的getX(int pointerIndex)、getY(int pointerIndex)得出的并不一定是pointer ID对应的坐标。
getX和getY的实现类似于从数组中取出信息,虽然A、B手指离开,但getX、getY传入的参数为0和1,
相当于取出数组中的第0、1位数据。
此时,保存坐标信息的数组在0、1位上,存储的是C、D手指的信息。
这也说明了,当检测两个手指的手势时,不能在第三个指头放上屏幕,第一个指头离开时,
利用getX、getY获取pointer ID = 2的数据,底层的数据仍然需要利用0、1为索引获取。
否则将抛出 java.lang.IllegalArgumentException: pointerIndex out of range的异常。
更多相关文章
- Android数据与界面绑定工具简述
- 学Android开发不可不知的Android应用程序四大组件
- Android(安卓)API Guides---Layouts
- 第3.1.3节 排布视图
- 如何使用Android(安卓)UI Fragment开发“列表-详情”界面
- Android视图控件属性layout_weight的作用
- android UI设计之思考
- Android多屏幕适配及自适应解决方案
- Android绘图机制(一)——自定义View的基础属性和方法