Blog of CodeJELLOW’S ENGINEERING NOTES
← 返回文章

Java安卓学习总结(三十五)

Java安卓学习总结(三十五) 第三十一章 定制视图与触摸事件

第三十一章 定制视图与触摸事件

有关定制视图

定制试图是为了实现独特的应用视觉效果。

分类

简单视图:不包括子视图,几乎总是用来处理定制绘制。由于View是一个空白画布,经常将简单视图的超类设置为View,例如:

TEXT
public class BoxDrawingView extends View

聚合试图:由其它视图对象组成。太长用于管理里子视图,但是不负责处理定制绘制。绘制任务一般托管给子视图。一般用FrameLayout作为超类。

创建完一个视图之后就可以在布局文件中添加这个视图了。

TEXT
<com.shopkeeper.dragandgraw.BoxDrawingView    xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/box_drag_view"    android:layout_width="match_parent"    android:layout_height="match_parent"/>

有关处理触摸事件

监听触摸方法可以使用View的触摸事件监听器:

TEXT
public void setOnTouchListener(View.onTouchListener l)

也可以直接覆盖view的onTouchEvent方法:

TEXT
public boolean onTouchEvent(MotionEvent event)

这个方法接受MotionEvent实例。MotionEvent类是用来描述包括位置和动作的触摸事件,其getAction()方法可以查看动作值:

TEXT
@Override
public boolean onTouchEvent(MotionEvent event) {
    PointF current=new PointF(event.getX(),event.getY());
    String action="";

    switch (event.getActionMasked()){
        case MotionEvent.ACTION_DOWN:
           ...
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            ...
            break;
        case MotionEvent.ACTION_POINTER_UP:
            ...
            break;
        case MotionEvent.ACTION_MOVE:
            ...
            break;
        case MotionEvent.ACTION_UP:
           ...
            break;
        case MotionEvent.ACTION_CANCEL:
            ...
            break;
    }
...
    return true;
}

值得注意的是,getAction用来监控手指的触摸的行为,getAction = getActionIndex() + getActionMasked()。getAction 是16位数,高八位为getActionIndex,指第几个手指的操作;低八位为getActionMasked,表示手指的触摸行为。当只有一只手指时(如代码所示),getActionIndex==0,所以getAction == getActionMasked。参考资料

有关onDraw方法图形绘制

onDraw要用到两大绘制类:Canvas和Paint。Canvas进行绘制操作,如在何处绘制什么图形、整个画布的旋转缩放等等;Paint决定如何绘制(绘制的内容),由填充、颜色字体等等。

不过一般这两个类都在构造方法中就被实例化了,只是需要在onDraw中调用:

TEXT
public BoxDrawingView(Context context, AttributeSet attributeSet){    super(context,attributeSet);    mBoxPaint=new Paint();    mBoxPaint.setColor(0x22ff0000);    mBackgroundPaint=new Paint();    mBackgroundPaint.setColor(0xfff8efe0);}

在onDraw中画图:

TEXT
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPaint(mBackgroundPaint);

    for (Box box:mBoxes)
    {
        float left=Math.min(box.getOrigin().x,box.getCurrent().x);
        float right=Math.max(box.getOrigin().x,box.getCurrent().x);
        float top=Math.min(box.getOrigin().y,box.getCurrent().y);
        float bottom=Math.max(box.getOrigin().y,box.getCurrent().y);

        canvas.drawRect(left,top,right,bottom,mBoxPaint);
    }
}

在onTouchEvent调用invalidate();可以使得布局无效而触发onDraw方法重新绘制界面。

有关挑战联系 设备旋转问题

首先说一下,这个题设里面给的简单方法害人不浅,我私底下认为是胡说的,按照它的说法只要用Bundle就可以了,但是我无论怎么试,就算达到了旋转设备时会保留所有矩形框的信息,但是按下历史任务栏键应用就会崩溃。所以还是要老老实实写Parceable,其实也不复杂,Android studio已经强大到会帮你自动实现Parceable接口了:

TEXT
public class Box implements Parcelable {    private PointF mOrigin;    private PointF mCurrent;    public Box(PointF origin)    {        mCurrent=origin;        mOrigin=origin;    }    protected Box(Parcel in) {        mOrigin = in.readParcelable(PointF.class.getClassLoader());        mCurrent = in.readParcelable(PointF.class.getClassLoader());    }    public static final Creator<Box> CREATOR = new Creator<Box>() {        @Override        public Box createFromParcel(Parcel in) {            return new Box(in);        }        @Override        public Box[] newArray(int size) {            return new Box[size];        }    };    public PointF getCurrent()    {        return  mCurrent;    }    public PointF getOrigin()    {        return mOrigin;    }    public void setCurrent(PointF current)    {        mCurrent=current;    }    @Override    public int describeContents() {        return 0;    }    @Override    public void writeToParcel(Parcel parcel, int i) {        parcel.writeParcelable(mOrigin, i);        parcel.writeParcelable(mCurrent, i);    }}

重写方法存储/取出状态:

TEXT
@Nullable@Overrideprotected Parcelable onSaveInstanceState() {    Bundle bundle= new Bundle();    bundle.putParcelable(SUPER_BUNDLE,super.onSaveInstanceState());    bundle.putParcelableArrayList(SAVE_BUNDLE, mBoxes);    return (Parcelable)bundle;}@Overrideprotected void onRestoreInstanceState(Parcelable state) {    Bundle bundle=(Bundle)state;    Parcelable superData = bundle.getParcelable(SUPER_BUNDLE);    mBoxes =bundle.getParcelableArrayList(SAVE_BUNDLE);    super.onRestoreInstanceState(superData);}

有关挑战练习 旋转矩形框

我一开始卡了好久,怎么弄都转不起来,直到看到书上要我看的第一个方法的提示之后我才明白,多个手指操作之后getAction得到的并不单纯是操作的动作值了,需要修改成getActionMasked:

TEXT
switch (event.getActionMasked()){
    case MotionEvent.ACTION_DOWN:
.....

其它的几个方法可以参照这个帖子来看。

我的完整方法如下:

TEXT
@Overridepublic boolean onTouchEvent(MotionEvent event) {    PointF current=new PointF(event.getX(),event.getY());    String action="";    switch (event.getActionMasked()){        case MotionEvent.ACTION_DOWN:            action="ACTION_DOWN";            mCurrentBox=new Box(current);            mBoxes.add(mCurrentBox);            mFirstIndex=event.getActionIndex();            break;        case MotionEvent.ACTION_POINTER_DOWN:            shouldRotate=true;            mSecondIndex=event.getActionIndex();            if (mCurrentBox!=null)            {                mBoxes.remove(mCurrentBox);            }            break;        case MotionEvent.ACTION_POINTER_UP:            shouldRotate=false;            break;        case MotionEvent.ACTION_MOVE:            action="ACTION_MOVE";            if (shouldRotate)            {                mAngle=mAngle+getAngle(event,mFirstIndex,mSecondIndex);                invalidate();            }            else {                if (mCurrentBox!=null)                {                    mCurrentBox.setCurrent(current);                    invalidate();                }            }            break;        case MotionEvent.ACTION_UP:            action="ACTION_UP";            mCurrentBox=null;            break;        case MotionEvent.ACTION_CANCEL:            action="ACTION_CANCEL";            mCurrentBox=null;            break;    }    Log.i(TAG,action+" at x="+current.x+", y="+current.y);    return true;}

思路是每当有手指第一次按下时就将其Index记录为第一个的;如果有第二个的就将其Index记录为第二个,同时标记应该旋转且将第一个手指按下时创建的Box删除(因为单纯的旋转不需要画新的方框);第二个手指被抬起时就标记位不应该旋转;每当接收到滑动消息时根据是否需要旋转的标记来选择操作:如果不需要旋转就照常,如果需要旋转,就计算旋转后的角度(计算角度方法如下),并调用invalidate()方法重新绘制旋转后的图像。

计算角度时根据之前记载下的第一个和第二个手指的index,得到它们的id,再有它们的x、y坐标来计算角度:

TEXT
private static double getAngle(MotionEvent event,int firstIndex,int secondIndex) {    int firstId=event.getPointerId(firstIndex);    int secondId=event.getPointerId(secondIndex);    double deltaX = (event.getX(firstId) - event.getX(secondId));//获取两个手指触摸点的X坐标值的差值    double deltaY = (event.getY(firstId) - event.getY(secondId));//获取两个手指触摸点的Y坐标值的差值    return Math.atan2(deltaY, deltaX);}

旋转在onDraw方法中实现:

TEXT
@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    canvas.save();    canvas.rotate((float) mAngle,getWidth()/2,getHeight()/2);    canvas.drawPaint(mBackgroundPaint);    for (Box box:mBoxes)    {        float left=Math.min(box.getOrigin().x,box.getCurrent().x);        float right=Math.max(box.getOrigin().x,box.getCurrent().x);        float top=Math.min(box.getOrigin().y,box.getCurrent().y);        float bottom=Math.max(box.getOrigin().y,box.getCurrent().y);        canvas.drawRect(left,top,right,bottom,mBoxPaint);    }    canvas.restore();}

canvas.rotate方法的第一个参数是旋转角度,后面两个参数是旋转的轴点。

我还写了canvas的save和restore方法,参考资料,这两个方法可以理解是一个是保存画布的状态,一个是还原画布的状态,然而改变或还原画布是不会对画纸(画面的内容)造成影响的。不过这两个方法在我的代码里面没什么作用,因为所有方块都是一股脑全都画完的。

通过 RSS 订阅更新 →