最近学习《Android编程权威指南》这本书,很不错的一本书哟~ 看了真的很是受益匪浅,为了让书中的例子以及技术知识点完全融入自身技能,手动敲书中的例子代码以及作总结笔记是很有必要的事情,嘻嘻嘻~ 下面开始啦!
这一章的Demo主要是为了学习处理触摸事件,所以定制了一个View响应用户的触摸与拖动,在屏幕上绘制出矩形框。最终的Demo截图:
创建定制视图
定制视图两大类别:
- 简单视图。也可能复杂,不包括子视图,几乎总是用来处理定制绘制。
- 聚合视图。由其他视图对象组成,通常管理子视图,不负责执行绘制,绘制工作都是委托给了各个子视图。
定制视图三大步骤:
- 选择超类,继承View或者FrameLayout等等。
- 继承选定的超类,覆盖超类构造方法。
- 覆盖其他关键方法,以定制视图行为。
整本书的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
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包中寻找目标)
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<Box> 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<Box>) bundle.getSerializable(INDEX_BOX);
state = bundle.getParcelable(INDEX_SUPER_STATE);
}
super.onRestoreInstanceState(state);
}
当然还有注意:将Box实体类实现Serializable接口,原来定义mBoxes是private List<Box> mBoxes = new ArrayList<>();现在改为private ArrayList<Box> mBoxes = new ArrayList<>();因为ArrayList有实现Serializable接口,恰可以放入bundle中存储。还有一点,到布局文件中,给我们定制的view控件加个id属性。于是就可以完成第一个挑战练习了。
挑战总结:
- 重写的两个方法不同于Activity和Fragment的onSaveInstanceState(Bundle)方法。View视图有ID时,才可以调用
- 推荐使用Bundle,这样就不用自己实现Parcelable接口了(此接口实现起来比较复杂,尽量避免)
- 需保存BoxDrawingView的View父视图的状态,在Bundle中保存super.onSaveInstanceState()方法结果,然后调用super.onRestoreInstanceState(Parcelable)方法把结果发送给超类。
一开始,我就少了第三点,于是在旋转过程中一直奔溃,报错为
state = bundle.getParcelable(INDEX_SUPER_STATE);两句代码而已,当然别忘记了给控件加个id。
挑战练习:旋转矩形框
第三版将要补充的练习题,再来吧...