Android自定义View,滑动,事件传递小结
本文只总结知识点 欢迎补充,欢迎纠正。谢谢!
#预备知识
Android控件框架
####1. View树状图
- Android的View树结构总是以一个ViewGroup开始,包含多个View或ViewGroup
- View是所有控件的父类
- ViewGroup是继承自View的容器类抽象类
####2. AndroidUI界面架构图
- 每个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
文件,声明我们的自定义属性
<?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( TypedValue.COMPLEX_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...
@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
触摸事件
@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);//计算滑动速度velocityTracker.computeCurrentVelocity(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() {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 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()
低
Android自定义View,滑动,事件传递小结相关推荐
- Android自定义view之事件传递机制
Android自定义view之事件传递机制 在上一篇文章<Android自定义view之measure.layout.draw三大流程>中,我们探讨了一下view的显示过程.不太熟悉的同学 ...
- 精通Android自定义View(十三)事件分发简述
1 事件序列 (1)手指接触屏幕后会产生一系列事件,事件分为3种:ACTION_DOWN(手指刚刚接触屏幕).ACTION_MOVE(手指在屏幕移动).ACTION_UP(手指从屏幕松开) (2)一个 ...
- Android自定义View2--触摸事件传递机制
转载文章 :https://juejin.im/post/6844904041487532045#heading-6 https://juejin.im/post/684490389410388378 ...
- Android 自定义View 滑动解锁
自定义view来绘制一个类似滑动解锁的button,注释都在,效果如下 代码在下面,替换一下资源,可以直接使用,如果需要画的是圆角的话,需要把下面两行注释的DrawLine的注释打开,然后把两行dra ...
- Android自定义view之ViewPager指示器——1
Android自定义view之ViewPager指示器--1 在上两篇文章<Android自定义view之measure.layout.draw三大流程>以及<Android自定义v ...
- Android自定义View注意事项
Android自定义View系列 Android自定义View之Paint绘制文字和线 Android自定义View之图像的色彩处理 Android自定义View之Canvas Android自定义V ...
- 精通Android自定义View(十四)绘制水平向右加载的进度条
1引言 1 精通Android自定义View(一)View的绘制流程简述 2 精通Android自定义View(二)View绘制三部曲 3 精通Android自定义View(三)View绘制三部曲综合 ...
- Android自定义View之Paint绘制文字和线
Android自定义View系列 Android自定义View注意事项 Android自定义View之图像的色彩处理 Android自定义View之Canvas Android自定义View之轻松实现 ...
- android 自定义view滚动条,Android自定义View实现等级滑动条的实例
Android自定义View实现等级滑动条的实例 实现效果图: 思路: 首先绘制直线,然后等分直线绘制点: 绘制点的时候把X值存到集合中. 然后绘制背景图片,以及图片上的数字. 点击事件down的时候 ...
- Android 自定义View(四)实现股票自选列表滑动效果
一.前言 Android 开发过程中自定义 View 真的是无处不在,随随便便一个 UI 效果,都会用到自定义 View.前面三篇文章已经讲过自定义 View 的一些案例效果,相关类和 API,还有事 ...
最新文章
- 利用WSS搭建学生作业平台
- 用存储过程创建的分页
- ubuntu rar文件乱码
- 三天打鱼,两天晒网。
- linux下set和eval的使用小案例精彩解答
- 兼容性好的overflow CSS清除浮动一例
- JFinal 1.5 发布,JAVA极速WEB+ORM框架
- drop.delete.trauncat的区别
- google 浏览器默认打开控制台_前端开发调试:浏览器console方法总结
- dategridview代码选中行_使用IntelliJ IDEA进行Java代码调试的技巧
- springmvc注解详解
- 单文件浏览器_浏览器工作原理
- HDU 5468 Puzzled Elena (2015年上海赛区网络赛A题)
- 5S管理跟精益生产的关系是什么?如何使5S管理有效落地?
- [算法]代码运行时间增长数量级对比 线性级别N vs 线性对数级别 NlgN
- 5g局域网传输速度_4G5G和上网带宽与下载速度的换算方法
- python交易是什么意思_Py交易是什么意思
- linux环境下mysql主从数据库配置(maser-slave-replication)
- 读List源码之Vector,ArrayList,LinkedList
- 【Linux进程间通信】四、mmap共享存储映射