本文只总结知识点 欢迎补充,欢迎纠正。谢谢!
#预备知识
Android控件框架
####1. View树状图
Android的View树结构总是以一个ViewGroup开始,包含多个View或ViewGroupView是所有控件的父类ViewGroup是继承自View的容器类抽象类
####2. AndroidUI界面架构图
每个Activity都包含一个Window对象,通常为PhoneWindowPhoneWindow将一个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=200dplayout_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
文件,声明我们的自定义属性
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="titleText" format="string" /> <attr name="titleTextColor" format="color" /> <attr name="titleTextSize" format="dimension" /> <declare-styleable name="CustomTitleView"> <attr name="titleText" /> <attr name="titleTextColor" /> <attr name="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( PLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); break; } } a.recycle(); } 复制代码
几个点
单参数构造是直接new的时候会调用从xml中申明,并通过findViewById
实例化会调用第两个参数的构造方法通过TypedArray
解析自定义属性,完成时候记得回收解析自定义属性时get的类型与定义时的format
对应
###3. 测量onMeasure
确定View
大小以自定义View实现文字绘制为例:
@Overrideprotected 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
位置
@Overrideprotected 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...
@Overrideprotected 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
触摸事件
@Overridepublic boolean onTouchEvent(MotionEvent event) {return super.onTouchEvent(event);}@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);}@Overridepublic 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";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_scroller);}@Overridepublic boolean onTouchEvent(MotionEvent event) {//获取VelocityTrackervelocityTracker = VelocityTracker.obtain();velocityTracker.addMovement(event);//计算滑动速度puteCurrentVelocity(1000);//计算速度float xVelocity = velocityTracker.getXVelocity();float yVelocity = velocityTracker.getYVelocity();Log.e(TAG,"&&&-->x = "+xVelocity+"---> y = "+yVelocity);return super.onTouchEvent(event);}@Overrideprotected void onDestroy() {super.onDestroy();if (null != velocityTracker){velocityTracker.clear();//重置velocityTracker.recycle();//回收内存}}}复制代码
用于追踪手指在滑动过程中的速度,包括水平速度和竖直方向的速度滑动速度值的正负取决于是否与坐标系方向一致滑动速度是相对一定时间的
GestureDetector
手势监控public class ScrollerActivity extends AppCompatActivity {private Toast toast;private GestureDetector mGestureDetector;@Overrideprotected 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() {@Overridepublic boolean onDown(MotionEvent e) {//手指轻触屏幕的一瞬间,由一个ACTION_DOWN触发showToast("轻触一下");return true;}@Overridepublic void onShowPress(MotionEvent e) {//手指轻触屏幕,尚未松开或拖动,由一个ACTION_DOWN触发showToast("轻触未松开");}@Overridepublic boolean onSingleTapUp(MotionEvent e) {//手指离开屏幕,伴随一个ACTION_UP触发,单击行为showToast("单击");return true;}@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//手指按下屏幕并拖动// 由一个由一个ACTION_DOWN,多个ACTION_MOVE触发,是拖动行为showToast("拖动");return false;}@Overridepublic void onLongPress(MotionEvent e) {//长按showToast("长按");}@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {//按下屏幕,快速滑动后松开,由一个由一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP触发showToast("快速滑动");return false;}};@Overridepublic 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());}@Overridepublic void computeScroll() {puteScroll();if (puteScrollOffset()) {//判断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 destYmScroller.startScroll(0, 0, deltaX, deltaY, 1000);invalidate();}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return true;}@Overridepublic 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() {@Overridepublic boolean tryCaptureView(View child, int pointerId) {//可以用来指定哪一个childView可以拖动return true;}@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动return left;}@Overridepublic int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动return top;}};@Overridepublic boolean onInterceptHoverEvent(MotionEvent event) {//拦截事件return mViewDragHelper.shouldInterceptTouchEvent(event);}@Overridepublic 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. 主要方法先来看一张图
事件分发
@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);}复制代码
返回结果表示是否拦截当前事件。返回
true
,拦截;false
,不拦截 事件分发的第一步,当事件传递到当前View一定会调用。返回结果受此View
的onTouchEvent()
方法和下级childView
的dispachTouchEvent
影响。虽然是事件分发第一步,但绝多数情况不推荐直接修改这个方法
事件拦截
@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return super.onInterceptTouchEvent(ev);}复制代码
返回结果用来判断是否拦截某个事件。这个方法只存在于ViewGroup中如果当前
view
拦截了某个事件,在同一个事件的序列中,此方法便不会被再次调用
事件消费
@Overridepublic 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()
低