《Android编程权威指南》之定制视图与触摸事件

最近学习《Android编程权威指南》这本书,很不错的一本书哟~ 看了真的很是受益匪浅,为了让书中的例子以及技术知识点完全融入自身技能,手动敲书中的例子代码以及作总结笔记是很有必要的事情,嘻嘻嘻~ 下面开始啦!

这一章的Demo主要是为了学习处理触摸事件,所以定制了一个View响应用户的触摸与拖动,在屏幕上绘制出矩形框。最终的Demo截图:

《Android编程权威指南》之定制视图与触摸事件_第1张图片
Demo截图
实际上,此章内容就是在描述自定义View的一些基础知识来着,android中自带了很多优秀的视图与组件,但为了追求独特的应用视觉效果,根据项目需求,我们还是需要自定义一些视图。

创建定制视图

定制视图两大类别:

  1. 简单视图。也可能复杂,不包括子视图,几乎总是用来处理定制绘制。
  2. 聚合视图。由其他视图对象组成,通常管理子视图,不负责执行绘制,绘制工作都是委托给了各个子视图。

定制视图三大步骤:

  1. 选择超类,继承View或者FrameLayout等等。
  2. 继承选定的超类,覆盖超类构造方法。
  3. 覆盖其他关键方法,以定制视图行为。

整本书的Demo在设计过程中,都是推荐使用Fragment的,而且导入是v4包中的fragment,当然也是为了兼容。

SingleFragmentActivity.java

public abstract class SingleFragmentActivity extends AppCompatActivity {
    protected abstract Fragment createFragment();
    @LayoutRes
    protected int getLayoutRedId(){
        return R.layout.activity_fragment;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutRedId());
        FragmentManager fm = getSupportFragmentManager();
        Fragment fragment = fm.findFragmentById(R.id.fragment_container);
        if (fragment == null){
            fragment = createFragment();
            fm.beginTransaction().add(R.id.fragment_container, fragment).commit();
        }
    }
}

activity_fragment.xml

《Android编程权威指南》之定制视图与触摸事件_第2张图片
Demo截图
DragAndDrawActivity.java

public class DragAndDrawActivity extends SingleFragmentActivity {
    @Override
    protected Fragment createFragment() {
        return DragAndDrawFragment.newInstance();
    }
}

DragAndDrawFragment.java

public class DragAndDrawFragment extends Fragment {
    public static DragAndDrawFragment newInstance(){
        return new DragAndDrawFragment();
    }
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_drag_and_draw, container, false);
        return v;
    }
}

fragment_drag_and_draw.xml(注意,使用BoxDrawingView的全路径名,这样布局inflater才能正确的解析布局XML文件,并按视图定义创建View实例,如果目标文件放置在其他保重,布局inflater无法找到目标会导致奔溃,inflater默认在android.view和android.widget包中寻找目标)

《Android编程权威指南》之定制视图与触摸事件_第3张图片
Demo截图
Box.java(矩形框的实例类,用于保存多个MotionEvent数据)

public class Box {
    private PointF mOrigin;  // 原始坐标点(手指的初始位置)
    private PointF mCurrent; // 当前坐标点(手指的当前位置)
    public Box(PointF origin) {
        mOrigin = origin;
        mCurrent = origin;
    }
    public PointF getOrigin() {
        return mOrigin;
    }
    public void setOrigin(PointF origin) {
        mOrigin = origin;
    }
    public PointF getCurrent() {
        return mCurrent;
    }
    public void setCurrent(PointF current) {
        mCurrent = current;
    }
}

BoxDrawingView.java (这是一个简单视图,是View的直接子类,这里添加了两个构造方法,因为视图可从代码或者布局文件实例化。从布局文件实例化的视图可收到一个AttributeSet实例,该实例包含了XML布局文件中指定的XML属性,自定义view很多时候是需要定制属性的,当然此Demo没有这一步。本书推荐,即使不打算使用构造方法,按习惯也应添加。万一未来需要使用呢,是吧,算是一种编程规范吧)

public class BoxDrawingView extends View {
    private static final String TAG = "BoxDrawingView";
    private Box mCurrentBox;
    private List mBoxes = new ArrayList<>();
    private Paint mBoxPaint;
    private Paint mBackgroundPaint;
    public BoxDrawingView(Context context) {
        this(context, null);
    }
    public BoxDrawingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mBoxPaint = new Paint();
        mBoxPaint.setColor(0x22ff0000);
        mBackgroundPaint = new Paint();
        mBackgroundPaint.setColor(0xfff8ef20);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        PointF current = new PointF(event.getX(), event.getY());
        String action = "";
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                action = "ACTION_DOWN";
                mCurrentBox = new Box(current);
                mBoxes.add(mCurrentBox);
                break;
            case MotionEvent.ACTION_MOVE:
                action = "ACTION_MOVE";
                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;
    }
    @Override
    protected void onDraw(Canvas 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);
        }
    }
}

监听触摸事件:

  • 方法一 —— 设置一个触摸事件监听器:
    public void setOnTouchListener(View.OnTouchListener l),工作方式与setOnClickListener(View.OnClickListener)相同,实现View.OnTouchListener接口,供触摸时间发生时调用。
  • 方法二 —— 这个Demo是在定制视图View的子类,捷径就是覆盖view的方法:public boolean onTouchEvent(MotionEvent event)
    该方法接受一个MotionEvent类实例,用于描述包括位置和动作的触摸事件。
    ACTION_DOWN------手指触摸到屏幕
    ACTION_MOVE------手指在屏幕上移动
    ACTION_UP------手指离开屏幕
    ACTION_CANCEL------父视图拦截了触摸事件
    BoxDrawingView.java中,X和Y坐标已经封装到了PointF对象中

两大绘制类:

  • Canvas:拥有我们需要的所有绘制操作,可决定在哪里以及绘什么,比如线条、圆形、字词、矩形等
  • Paint:决定如何绘制,可指定绘制图形的特征,例如是否填充图形、使用什么字体绘制、线条是什么颜色等。
    此章的Demo还是很简单的,大致就是了解下制定视图的基础,也没详细的描述自定义view原理

挑战练习:设备旋转问题

当设备旋转后,上面绘制的矩形框会消失,提示使用View方法:
protected Parcelable onSaveInstancesState()

protected void onRestoreInstanceState()

先贴我的方案代码吧:
BoxDrawingView.java

private static final String INDEX_BOX = "box";
private static final String INDEX_SUPER_STATE = "superState";
重写两个方法:
 @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable(INDEX_SUPER_STATE, super.onSaveInstanceState());
        bundle.putSerializable(INDEX_BOX, mBoxes);
        return bundle;
    }
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if(state instanceof Bundle){
            Bundle bundle = (Bundle) state;
            this.mBoxes = (ArrayList) bundle.getSerializable(INDEX_BOX);
            state = bundle.getParcelable(INDEX_SUPER_STATE);
        }
        super.onRestoreInstanceState(state);
    }

当然还有注意:将Box实体类实现Serializable接口,原来定义mBoxes是private List mBoxes = new ArrayList<>();现在改为private ArrayList mBoxes = new ArrayList<>();因为ArrayList有实现Serializable接口,恰可以放入bundle中存储。还有一点,到布局文件中,给我们定制的view控件加个id属性。于是就可以完成第一个挑战练习了。

挑战总结:

  • 重写的两个方法不同于Activity和Fragment的onSaveInstanceState(Bundle)方法。View视图有ID时,才可以调用
  • 推荐使用Bundle,这样就不用自己实现Parcelable接口了(此接口实现起来比较复杂,尽量避免)
  • 需保存BoxDrawingView的View父视图的状态,在Bundle中保存super.onSaveInstanceState()方法结果,然后调用super.onRestoreInstanceState(Parcelable)方法把结果发送给超类。
    一开始,我就少了第三点,于是在旋转过程中一直奔溃,报错为
    《Android编程权威指南》之定制视图与触摸事件_第4张图片
    Demo截图
    这里也就少了 bundle.putSerializable(INDEX_BOX, mBoxes);
    state = bundle.getParcelable(INDEX_SUPER_STATE);两句代码而已,当然别忘记了给控件加个id。

挑战练习:旋转矩形框

第三版将要补充的练习题,再来吧...

你可能感兴趣的:(《Android编程权威指南》之定制视图与触摸事件)