Android

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

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

有关定制视图

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

分类

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

public class BoxDrawingView extends View

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

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

<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的触摸事件监听器:

public void setOnTouchListener(View.onTouchListener l)

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

public boolean onTouchEvent(MotionEvent event)

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

@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中调用:

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

mBackgroundPaint=new Paint();
mBackgroundPaint.setColor(0xfff8efe0);
}

在onDraw中画图:

@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接口了:

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);
}
}

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

@Nullable
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle= new Bundle();
bundle.putParcelable(SUPER_BUNDLE,super.onSaveInstanceState());
bundle.putParcelableArrayList(SAVE_BUNDLE, mBoxes);
return (Parcelable)bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
Bundle bundle=(Bundle)state;
Parcelable superData = bundle.getParcelable(SUPER_BUNDLE);
mBoxes =bundle.getParcelableArrayList(SAVE_BUNDLE);
super.onRestoreInstanceState(superData);
}

有关挑战练习 旋转矩形框

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

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

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

我的完整方法如下:

@Override
public 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坐标来计算角度:

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方法中实现:

@Override
protected 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方法,参考资料,这两个方法可以理解是一个是保存画布的状态,一个是还原画布的状态,然而改变或还原画布是不会对画纸(画面的内容)造成影响的。不过这两个方法在我的代码里面没什么作用,因为所有方块都是一股脑全都画完的。

发表评论