300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > Android自定义View 滑动 事件传递小结

Android自定义View 滑动 事件传递小结

时间:2018-08-08 19:56:09

相关推荐

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_parentAT_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);}复制代码

几个点

当直接继承ViewViewGroup重写onMeasure时,注意Viewwidthheightwarp_content时,需要特殊处理,否则默认为父布局剩余空间。

Why? **View自身的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams(也就是xml中设置的layout_width=warp_content,或代码中获取ViewLayoutParams设置宽高)共同决定。**

View设置宽高为具体数值时,无论父容器的MeasureSpec是什么,ViewMeasureSpec都是EXACTLY,宽高为LayoutParams中的大小。当View设置宽高为match_parent时,①.父布局的MeasureSpecEXACTLY时,ViewMeasureSpec也为EXACTLY,大小为父容器剩余空间;②.父布局的MeasureSpecAT_MOST时,ViewMeasureSpec也为AT_MOST,大小不会超过父容器剩余空间;当View设置宽高为warp_content时,无论父容器的MeasureSpec是什么,ViewMeasureSpec都是AT_MOST,并且大小不能超过父容器剩余空间注:依据Android开发艺术探索如需支持Padding需要在测量时计算继承ViewGroup,如需支持Margin需要在测量时计算View的生命周期与Activity不是同步,所以在ActivityonResume及之前的生命周期方法中获取View的宽高是不靠谱的

获取方法

重写onWindowFocusChanged在这个方法中获取view.post(runnable)ViewTreeObserver4.view.measure(int widthMeasureSpec,int heightMeasureSpec)不建议,因为这个方法要区分LayoutParams,在这不具体阐述计算类TextView的自定义布局的高度时,需知FontMetrics这个类:

FontMetrics有五个float类型值:

leading留给文字音标符号的距离

ascentbaseline线到最高的字母顶点到距离,负值

topbaseline线到字母最高点的距离加上ascent|top|=|ascent|+|leading|

descentbaseline线到字母最低点到距离

bottomtop类似,系统为一些极少数符号留下的空间。topbottom总会比ascentdescent大一点的就是这些少到忽略的特殊符号

###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中使用HandlerView中如果有线程或者动画,需要及时停止,否则有可能造成内存泄漏,在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本身无法实现弹性滑动,需要配合ViewcomputeScroll()方法

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()方法。重写childViewdispatchTouchEvent()方法 在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_MOVEACTION_UP都会又父容器来处理,这样事件就无法传递给childView其次,在ACTION_MOVE中,可以根据需要来进行拦截,需要就返回true,否则就false最后,在ACTION_UP中,返回false(如果父容器在ACTION_UP中,返回了truechildView就不会再收到ACTION_UP事件,childViewonClick事件就不会触发。父容器比较特殊,一旦开始拦截某个事件,之后的序列事件都是交给父容器来处理,包括ACTION_UP,即使在ACTION_UP中返回falseACTION_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一定会调用。返回结果受此ViewonTouchEvent()方法和下级childViewdispachTouchEvent影响。虽然是事件分发第一步,但绝多数情况不推荐直接修改这个方法

事件拦截

@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 <- ViewView设置的onTouchListener()优先级高于onTouchEvent()onClickListener()优先级比onToucnEvent()

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。