本文只总结知识点 欢迎补充,欢迎纠正。谢谢!
#预备知识
Android控件框架
####1. View树状图
- Android的View树结构总是以一个ViewGroup开始,包含多个View或ViewGroup
- View是所有控件的父类
- ViewGroup是继承自View的容器类抽象类
- 每个Activity都包含一个Window对象,通常为PhoneWindow
- PhoneWindow将一个DecorView作为整个窗口的根View,DecorView作为窗口顶层视图封装了一些窗口操作的方法
- DecorView将内容显示在PhoneWindow上,并通过WindowManagerService来进行接收,并通过Activity对象来回调对应的onClickListener。显示时,将屏幕分成两个部分,TitleView和ContentView。Content是一个id为content的FrameLayout,activity_main.xml就在其中。
坐标体系
View的坐标由它的四个顶点决定,分别对应View的四个属性
获得四个顶点的方式
- left,getLeft() 左上角的横坐标
- top,getTop()左上角的纵坐标
- right,getRight() 右下角的横坐标
- bottom,getBottom() 右下角的纵坐标
View测量
View测量主要依赖MeasureSpec
测量模式有三种
EXACTLY
精确模式
- 明确指定数值: layout_width=200dp,layout_height=200dp
- layout_width=match_parent,layout_height=match_parent
AT_MOST
最大模式
- layout_width=warp_content,layout_height=warp_content
- 空间大小会随着内容变大而变大,最大为父布局剩余空间
UNSPECIFIED
父容器不对View限制大小,要多大给多大,这种情况一般用于系统内部,表示一种测量状态,
不用过多关注
#一、自定义View ##分类
- 继承View,重写
onDraw
方法 - 继承已有
View
(比如TextView
) - 继承
ViewGroup
实现特殊的Layout
- 继承已有的
ViewGroup
(比如LinearLayout
)
##一般步骤 ###1. 在res/values/
下建立一个attrs.xml
文件,声明我们的自定义属性
"1.0" encoding="utf-8"?>
"titleText" format="string" />
"titleTextColor" format="color" />
"titleTextSize" format="dimension" />
<declare-styleable name="CustomTitleView">
"titleText" />
"titleTextColor" />
"titleTextSize" />
declare-styleable>
复制代码
###2. 继承View
(或其他)重写构造方法
public CustomView(Context context)
{
this(context, null);
}
/**
* 获得我自定义的样式属性
*
* @param context
* @param attrs
* @param defStyle
*/
public CustomView(Context context, AttributeSet attrs)
{
super(context, attrs, defStyle);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.CustomView_titleText:
mTitleText = a.getString(attr);
break;
case R.styleable.CustomView_titleTextColor:
// 默认颜色设置为黑色
mTitleTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.CustomView_titleTextSize:
// 默认设置为16sp,TypeValue也可以把sp转化为px
mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
}
复制代码
几个点
- 单参数构造是直接new的时候会调用
- 从xml中申明,并通过
findViewById
实例化会调用第两个参数的构造方法 - 通过
TypedArray
解析自定义属性,完成时候记得回收 - 解析自定义属性时get的类型与定义时的
format
对应
###3. 测量onMeasure
确定View
大小 以自定义View实现文字绘制为例:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize=MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode=MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize=MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
int textWidth=(int)mPaint.measureText(mText)+getPaddingLeft()+getPaddingRight();
int textHeight=(int)(-mPaint.ascent() + mPaint.descent())+getPaddingTop()+getPaddingBottom();
if(wSpecMode==AT_MOST&&hSpecMode==AT_MOST){
width=textWidth;
height=textHeight;
}else if(wSpecMode==AT_MOST){
width=textWidth;
height=hSpecSize;
}else if(hSpecMode==AT_MOST){
width=wSpecSize;
height=textHeight;
}
width=Math.min(width,wSpecSize);
height=Math.min(height,hSpecSize);
setMeasuredDimension(width,height);
}
复制代码
几个点
- 当直接继承
View
或ViewGroup
重写onMeasure
时,注意View
的width
或height
为warp_content
时,需要特殊处理,否则默认为父布局剩余空间。Why? **
View
自身的MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
(也就是xml中设置的layout_width=warp_content
,或代码中获取View
的LayoutParams
设置宽高)共同决定。**- 当
View
设置宽高为具体数值时,无论父容器的MeasureSpec
是什么,View
的MeasureSpec
都是EXACTLY
,宽高为LayoutParams
中的大小。 - 当
View
设置宽高为match_parent
时,①.父布局的MeasureSpec
为EXACTLY
时,View
的MeasureSpec
也为EXACTLY
,大小为父容器剩余空间;②.父布局的MeasureSpec
为AT_MOST
时,View
的MeasureSpec
也为AT_MOST
,大小不会超过父容器剩余空间; - 当
View
设置宽高为warp_content
时,无论父容器的MeasureSpec
是什么,View
的MeasureSpec
都是AT_MOST
,并且大小不能超过父容器剩余空间 注:依据Android开发艺术探索
- 当
- 如需支持
Padding
需要在测量时计算 - 继承
ViewGroup
,如需支持Margin
需要在测量时计算 View
的生命周期与Activity
不是同步,所以在Activity
的onResume
及之前的生命周期方法中获取View
的宽高是不靠谱的
获取方法
- 重写
onWindowFocusChanged
在这个方法中获取 view.post(runnable)
ViewTreeObserver
4.view.measure(int widthMeasureSpec,int heightMeasureSpec)
不建议,因为这个方法要区分LayoutParams,在这不具体阐述
- 计算类
TextView
的自定义布局的高度时,需知FontMetrics
这个类:
在
FontMetrics
有五个float
类型值:
-
leading
留给文字音标符号的距离 -
ascent
从baselin
e线到最高的字母顶点到距离,负值 -
top
从baseline
线到字母最高点的距离加上ascent
,|top|
=|ascent|
+|leading|
-
descent
从baseline
线到字母最低点到距离 -
bottom
和top
类似,系统为一些极少数符号留下的空间。top
和bottom
总会比ascent
和descent
大一点的就是这些少到忽略的特殊符号
###4. 布局onLayout
确定View
位置
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
复制代码
几个点
- 容器类自定义
View
布局时需处理Margin
###5. 绘制onDraw
想要绘制一个view
,需要什么?
- 保存像素的Bitmap
- 管理绘制请求的Canvas
- 绘画的原始基本元素,例如矩形,线,文字,Bitmap
- 拥有颜色和风格信息的画笔
综合来说就是:画笔Paint
,画布Canvas
,画什么:text,bitmap,path...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//文字的x轴坐标
float stringWidth = mPaint.measureText(text);
float x = (getWidth() - stringWidth) / 2;
//文字的y轴坐标
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float y = getHeight() / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2;
canvas.drawText(text, x, y, mPaint);
}
复制代码
几个点
- 绘制区域关注
Rect,RectF
- 文字是从
baseline
开始绘制 - 考虑
padding
- 尽量不要在
onDraw
中构造对象 - 绘制时需要用到的一些类
- 绘制文字:
FontMetrics
(文字度量)
- 绘制图像:
ColorMatrix(图像色彩),PorterDuffXfermore(两个图像间的混合显示模式), Shader (着色器), Matrix(图形处理)
- 绘制路径:
Path(路径),PathEffect(路径效果), Bezier (贝塞尔曲线), PathMeasure (辅助计算Path的计算器)
- 继承
ViewGroup
处理滑动、拖动辅助类:ViewDragHelper(可以实现各种不同的的滑动、拖动)
###注意几点
- 尽量不要在
View
中使用Handler
View
中如果有线程或者动画,需要及时停止,否则有可能造成内存泄漏,在onDetachedFromWindow
中处理- 处理好焦点传递
- 处理滑动及滑动冲突
#二、View
滑动 ##1. 触摸、滑动相关
MotionEvent
触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
复制代码
用于报告(鼠标、笔、手指,轨迹球)运动事件
ACTION_DOWN(按下),ACTION_UP(抬起),ACTION_MOVE(移动),ACTION_CANCEL(取消)
TouchSlop
最小距离
ViewContfiguration.get(getConetxt()).getScaledTouchSlop()
复制代码
TouchSlop
是系统识别最小的滑动距离,是一个常量值。当手指在屏幕滑动距离小于这个值时,系统不会将动作视为滑动。这个常量值的具体大小和设备也有关,不同的屏幕分辨率,可能会不一样 利用这个临界值,可以将一些不想要的手指操作给过滤掉
VelocityTracker
速度追踪
public class ScrollerActivity extends AppCompatActivity {
private VelocityTracker velocityTracker;
private final String TAG = "ScrollerActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroller);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取VelocityTracker
velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//计算滑动速度
velocityTracker.computeCurrentVelocity(1000);//计算速度
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
Log.e(TAG,"&&&-->x = "+xVelocity+"---> y = "+yVelocity);
return super.onTouchEvent(event);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (null != velocityTracker){
velocityTracker.clear();//重置
velocityTracker.recycle();//回收内存
}
}
}
复制代码
用于追踪手指在滑动过程中的速度,包括水平速度和竖直方向的速度 滑动速度值的正负取决于是否与坐标系方向一致 滑动速度是相对一定时间的
GestureDetector
手势监控
public class ScrollerActivity extends AppCompatActivity {
private Toast toast;
private GestureDetector mGestureDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroller);
initGestureDetector();
}
/**
* 初始化 GestureDetector
*/
private void initGestureDetector() {
mGestureDetector = new GestureDetector(ScrollerActivity.this,onGestureListener );
//解决屏幕长按后无法拖动
mGestureDetector.setIsLongpressEnabled(false);
}
private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {//手指轻触屏幕的一瞬间,由一个ACTION_DOWN触发
showToast("轻触一下");
return true;
}
@Override
public void onShowPress(MotionEvent e) {//手指轻触屏幕,尚未松开或拖动,由一个ACTION_DOWN触发
showToast("轻触未松开");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {//手指离开屏幕,伴随一个ACTION_UP触发,单击行为
showToast("单击");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//手指按下屏幕并拖动
// 由一个由一个ACTION_DOWN,多个ACTION_MOVE触发,是拖动行为
showToast("拖动");
return false;
}
@Override
public void onLongPress(MotionEvent e) {//长按
showToast("长按");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//按下屏幕,快速滑动后松开,由一个由一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP触发
showToast("快速滑动");
return false;
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
}
/**
* Toast
*/
private void showToast(String str) {
if (null == toast) {
toast = Toast.makeText(ScrollerActivity.this, str, Toast.LENGTH_LONG);
} else {
toast.setText(str);
}
toast.show();
}
}
复制代码
用于辅助检测单击、滑动、长按、双击
GestureDetector.setOnDoubleTapListener(onDoubleTapListener)
可以实现双击 在OnGestureListener内onDown(),onSingleTapUp(),onScroll(),onFling()
方法都有一个boolean
类型的返回值,这个值表示是否消费事件
Scroller
弹性滑动对象
public class ScrollerView extends LinearLayout {
private Scroller mScroller;
public ScrollerView(Context context, AttributeSet attrs) {
super(context, attrs);
initScroller();
}
/**
* 初始化Scroller
*/
private void initScroller() {
mScroller = new Scroller(getContext());
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {//判断Scroller是否执行完毕
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int destX, int destY) {
//计算相对于左上角的偏移量
final int deltaX = getScrollX() - destX;
final int deltaY = getScrollY() - destY;
//在1000ms内滑向destX destY
mScroller.startScroll(0, 0, deltaX, deltaY, 1000);
invalidate();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
smoothScrollTo((int) event.getX(), (int) event.getY());
break;
case MotionEvent.ACTION_UP://恢复左上角
mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 1000);
invalidate();
break;
}
return true;
}
}
复制代码
用于实现
View
的弹性滑动。Scroller
本身无法实现弹性滑动,需要配合View
的computeScroll()
方法
ViewDragHelper ``ViewGroup
中拖动、滑动view
的辅助类
public class DragView extends LinearLayout {
private ViewDragHelper mViewDragHelper;
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
initDragHelper();
}
private void initDragHelper() {
mViewDragHelper = ViewDragHelper.create(DragView.this, 1.0f, mDragCallback);
}
/**
* ViewDragHelper回调接口
*/
private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {//可以用来指定哪一个childView可以拖动
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
return top;
}
};
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {//拦截事件
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {//消费事件
//将触摸事件传递给`ViewDragHelper`,必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
}
复制代码
##2. 滑动冲突 ###常见的滑动冲突场景
- 外部滑动方向与内部滑动方向不一致
- 外部滑动方向与内部滑动方向一致
- 上面两种情况嵌套
###解决办法
- 内部拦截
- 外部拦截
#####1. 内部拦截
内部拦截法指的是父容器不拦截任何事件,所有的事件都传递给
childView
,根据需要,childView
来选择是否消费,需要配合requestDisallowInterceptTouchEvent()
方法。重写childView
的dispatchTouchEvent()
方法 在ACTION_DOWN
中,使用parent.requestDisallowInterceptTouchEvent(true)
,让父容器不拦截ACTION_DOWN
事件,ACTION_DOWN
不受FLAG_DISALLOW_INTERCEPT
标记位控制
伪代码
public boolean dispatchTouchEvent(MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父容器需要此类点击事件){
parent.requestDisallowInterceptTopuchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
break;
}
mLastX = x ;
mLastY = y ;
return super.dispatchTouchEvent(event);
}
复制代码
#####2. 外部拦截
点击事件都会先经过父容器的拦截处理,如果父容器需要处理此事就拦截,否则就不进行拦截。重写父容器的
onInterceptTouchEvent()
方法
- 首先,在
ACTION_DOWN
中,父容器必须返回false
,不拦截ACTION_DOWN
事件。因为一旦拦截了ACTION_DOWN
后续的ACTION_MOVE
和ACTION_UP
都会又父容器来处理,这样事件就无法传递给childView
- 其次,在
ACTION_MOVE
中,可以根据需要来进行拦截,需要就返回true
,否则就false
- 最后,在
ACTION_UP
中,返回false
(如果父容器在ACTION_UP
中,返回了true
,childView
就不会再收到ACTION_UP
事件,childView
的onClick
事件就不会触发。父容器比较特殊,一旦开始拦截某个事件,之后的序列事件都是交给父容器来处理,包括ACTION_UP
,即使在ACTION_UP
中返回false
,ACTION_UP
还是由父容器处理)
伪代码
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.Move:
if(父容器需要当前点击事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
复制代码
#三、事件分发 ##1. 主要方法 先来看一张图
事件分发 @Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
复制代码
返回结果表示是否拦截当前事件。返回
true
,拦截;false
,不拦截 事件分发的第一步,当事件传递到当前View一定会调用。返回结果受此View
的onTouchEvent()
方法和下级childView
的dispachTouchEvent
影响。虽然是事件分发第一步,但绝多数情况不推荐直接修改这个方法
事件拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
复制代码
返回结果用来判断是否拦截某个事件。这个方法只存在于ViewGroup中 如果当前
view
拦截了某个事件,在同一个事件的序列中,此方法便不会被再次调用
事件消费
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
复制代码
返回结果表示是否消费了事件。
true
,消费了,不用在审核了;false
,不消费,给父容器处理
##2. 主要流程 首先来看一张图
- 如果事件不被中断的话,整个流程呈U型
- 传递顺序
Activity -> Window -> ViewGroup -> View
- 消费顺序
Activity <- Window <- ViewGroup <- View
View
设置的onTouchListener()
优先级高于onTouchEvent()
,onClickListener()
优先级比onToucnEvent()
低