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后,才能利用ViewonSaveInstanceStateonRestoreInstanceState(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的异常。

更多相关文章

  1. Android数据与界面绑定工具简述
  2. 学Android开发不可不知的Android应用程序四大组件
  3. Android(安卓)API Guides---Layouts
  4. 第3.1.3节 排布视图
  5. 如何使用Android(安卓)UI Fragment开发“列表-详情”界面
  6. Android视图控件属性layout_weight的作用
  7. android UI设计之思考
  8. Android多屏幕适配及自适应解决方案
  9. Android绘图机制(一)——自定义View的基础属性和方法

随机推荐

  1. android:singleLine="true",[...]没有全
  2. Android(安卓)UI学习 - GridView和ImageV
  3. Android中UI组件android:layout_gravity
  4. ACtivity布局之相对布局基本用法
  5. Android:layout_weight详解
  6. 在Android平台上实现H264解码
  7. Android(安卓)获取系统电量信息
  8. 在 Android 平台上开发 OpenCV
  9. android小功能实现之xml文件解析(Pull)
  10. android listView 显示数据 单击 长按