Android_View详解
本文由 Luzhuo 编写,转发请保留该信息.
原文: http://blog.csdn.net/Rozol/article/details/72848723
View的绘制
View的事件分发机制
View的时间冲突处理
Activity控件架构
- 可以使用SDK下Tools目录里的 hierarchyviewer.bat 进行布局分析
- 当我们在Activity中执行
setContentView(R.layout.activity_main)
时,是给ContentView设置布局 - 每个控件都会在界面上占一块矩形区域
-控件分为:
- ViewGroup容器控件:
- 可以包含多个View和ViewGroup
- ViewGroup以树的结构管理View和ViewGroup, 主要负责下层控件的测量,绘制,事件传递.
- View控件
- ViewGroup容器控件:
View的绘制
- 关键词: measure [ˈmɛʒɚ] / layout [ˈleˌaʊt] / draw [drɔ]
- 绘制流程: measure -> layout -> draw
measure
- 测量View的宽高
代码的重写
public class MyView extends View { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
我们看看 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 做了什么?
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // ↓↓↓ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } // ↓↓↓ public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; } }
- 看来源码中是用 MeasureSpec 这个类来计算大小的, 我们先来了解下这个类
MeasureSpec 这个类是用来测量View的
- 测量模式:
- UNSPECIFIED: 不指定测量模式, 你想多大就多大
- EXACTLY: 精确测量模式, layout_width=”100dp”/”match-parent”都是使用该模式
- AT_MOST: 最大值模式, layout_width=”wrap_content”时使用该模式
我们来测试下三种模式的区别,在测试之前先修改下
public static int getDefaultSize(int size, int measureSpec)
方法的代码我们在
case MeasureSpec.AT_MOST:
下添加两行代码,让他在 layout_width=”wrap_content” 时使用默认值和测量值的最小值, 其他代码不变case MeasureSpec.AT_MOST: result = Math.min(size, specSize); break;
- 效果:
- 测量模式:
- Measure 是对ViewGroup树进行自上而下遍历而确定View最大可用大小的
- 最后需要调用
setMeasuredDimension(width, height);
设置View的宽高值
layout
- 确定View在父容器的位置
代码的重写
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); }
Layout的位置是对ViewGroup树进行自上而下遍历而确定的
- 参数说明:
- changed: View大小是否发生改变
- left, top, right, bottom: 左上右下的坐标值
- 实际开发中,ViewGroup会需要重写onLayout来确定子View的位置,View没啥位置可确定的
draw
- 绘制View
代码的重写
@Override protected void onDraw(Canvas canvas) { }
仅需调用canvas进行绘制即可
- Canvas就像画板,Paint就像画笔,使用Paint在Canvas上作画,作画的水平决定这个View控件是否优美.
案例代码
package me.luzhuo.viewdemo;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Printer;
import android.view.MotionEvent;
import android.view.View;
/**
* =================================================
* <p>
* Author: Luzhuo
* <p>
* Version: 1.0
* <p>
* Creation Date: 2017/6/1 15:55
* <p>
* Description: View的绘制机制
* <p>
* Revision History:
* <p>
* Copyright: Copyright 2017 Luzhuo. All rights reserved.
* <p>
* =================================================
**/
public class ViewDemo extends View {
private Paint paint;
private Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); // 测试图片
public ViewDemo(Context context) {
super(context);
init();
}
public ViewDemo(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ViewDemo(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new Paint();
}
/**
* 1. 测量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(bitmap.getWidth() * 3, widthMeasureSpec), getDefaultSize(bitmap.getHeight() * 3, heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
switch (specMode) {
case View.MeasureSpec.UNSPECIFIED:
result = size;
break;
case View.MeasureSpec.AT_MOST:
result = Math.min(size, specSize);
break;
case View.MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2.0f, y - bitmap.getHeight() / 2.0f, paint);
}
private float x, y;
@Override
public boolean onTouchEvent(MotionEvent event) {
x = event.getX(); // 控件x坐标
y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
invalidate();
return true;
}
}
其他重要方法补充
- 回调方法:
protected void onFinishInflate() { } // XML控件加载完成后回调
protected void onSizeChanged(int w, int h, int oldw, int oldh) { } // 控件的大小改变时回调
- 更新方法:
invalidate(); // 重绘(会调用onMeasure()), 如果onMeasure()测量的大小没有发生变化,就不会调用onLayout(), 但是会调用onDraw()
requestLayout(); // 重新测量View的大小, 会调用onMeasure()和onLayout(),但是不会调用onDraw()
View的事件分发机制
- Android上的View可能是重叠在一起的,当我们在手机屏幕上按下时,哪个View该响应呢?事件分发机制就是为解决这个问题的.
触摸事件: 就是触摸屏幕的事件; 触摸事件分为:按下 / 滑动 / 抬起, Android将触摸事件封装为MotionEvent类
MotionEvent的几个重要方法:
ev.getX(); // 相对于父容器的x坐标
ev.getRawX(); // 屏幕x坐标
MotionEvent.ACTION_DOWN; // 按下
MotionEvent.ACTION_MOVE; // 滑动
MotionEvent.ACTION_UP; // 抬起
事件的传递流程
- 当我们点击最上面的View时, 完成的事件传递是这样的:
- Activity -> PhoneWindow -> DecorView -> ContentView -> ViewGroup -> ViewGroup2 -> View -> ViewGroup2 -> ViewGroup -> ContentView -> DecorView -> PhoneWindow -> Activity
- 可见如果事件在传递的过程中都没有被消费, 那么事件将被回传
- 以下我们将简化传递流程, 并演示 dispatchTouchEvent(事件分发) / onInterceptTouchEvent(事件拦截) / onTouchEvent(触摸事件)
- 默认
- ViewGroup2 的 dispatchTouchEvent 返回 true
- ViewGroup2 的 onInterceptTouchEvent 返回 true
- ViewGroup2 的 onTouchEvent 返回 true
- 默认
- 总结
- dispatchTouchEvent: 分发事件(return true:拦截(事件丢弃), false,不拦截(默认)) (注:返回true,事件不会交由onTouchEvent处理)
- onInterceptTouchEvent: 拦截事件 (View没有该回调接口) (return: true:拦截事件,交由onTouchEvent处理, false:传递给子View,由子View的dispatchTouchEvent接收(默认))
- onTouchEvent: 触摸事件(return true:消费了, false:向上级传递(默认))
View的事件冲突处理
事件冲突实际开发中主要为滑动冲突:
解决方案分为:
- 第一种: 写一个新的ViewGroup继承父ViewGroup, 并重写
onInterceptTouchEvent()
- 第二种: 子View(或ViewGroup)使用
getParent().requestDisallowInterceptTouchEvent(true);
请求父类不要拦截Touch事件 (父ViewGroup将不会执行onInterceptTouchEvent()回调)
- 第一种: 写一个新的ViewGroup继承父ViewGroup, 并重写
第一种讲解: 较简单,没什么好讲的,贴个代码做参考吧 (这是限制ViewPager是否可左右滑动的案例代码)
public class CustomViewPager extends ViewPager { private boolean setTouchModel = false; public CustomViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(setTouchModel){ return super.onInterceptTouchEvent(ev); }else{ return false; } } @Override public boolean onTouchEvent(MotionEvent ev) { if(setTouchModel){ return super.onTouchEvent(ev); }else{ return false; } } }
第二种讲解:
先看容器ViewGroup2的代码, 拦截滑动事件,并进行处理
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.e("ViewGroupEvent2", "onInterceptTouchEvent"); switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: return true; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e("ViewGroupEvent2", "onTouchEvent"); return true; }
然后触摸view执行的结果, 可见滑动事件都被ViewGroup2消费掉了,view都没有拿到
然后我们在子view中加入
getParent().requestDisallowInterceptTouchEvent(true)
@Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("ViewEvent", "dispatchTouchEvent"); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("requestDisallowInterceptTouchEvent(true)"); getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: System.out.println("requestDisallowInterceptTouchEvent(false)"); getParent().requestDisallowInterceptTouchEvent(false); break; } return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e("ViewEvent", "onTouchEvent"); return true; }
执行结果如下,可见view已经夺得了滑动事件的控制权
ViewGroup
measure
- ViewGroup是管理子View的, MeasureSpec.AT_MOST 模式时, ViewGroup会先子View进行遍历, 获取所有子View的大小, 然后决定自己的大小
- 当子View测量完成后可通过调用
view.getLayoutParams()
获取宽高信息
layout
- 可通过
view.layout(l, t, r, b);
设置子View的位置
draw
- 容器没啥可绘制的