转载:
View 是一种界面层的控件的一种抽象,一组 View 则称为 ViewGroup,同时 ViewGroup 继承了 View。意味着 View 可以是单个控件也可以是多个控件组成的组控件,通过这种关系形成了 View 树的结构。
- Android坐标系:以屏幕的左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向。
- View的宽高和坐标关系:width = right - left,height = bottom - top。
- 从android3.0开始,View增加了额外几个参数:x,y,translationX、translationY。其中x和y是View左上角的坐标,translationX和translationY是新View左上角相对于父容器的偏移量,它们默认值是0。
- 存在关系:x = left + translationX,y = top + translationY
- 由此可见,x和left不同体现在:left是View的初始坐标,在绘制完毕后就不会再改变;而x是View偏移后的实时坐标,是实际坐标。y和top的区别同理。
通过MotionEvent 对象可以得到触摸位置的x、y坐标。其中通过getX()、getY()可获取相对于当前view左上角的x、y坐标;通过getRawX()、getRawY()可获取相对于手机屏幕左上角的x,y坐标。
该常量和设备有关,可用它来判断用户的滑动是否达到阈值,获取方法:
ViewConfiguration.get(getContext()).getScaledTouchSlop()。
A、首先在view的onTouchEvent方法中追踪当前单击事件的速度:
//实例化一个VelocityTracker 对象
VelocityTracker velocityTracker = VelocityTracker.obtain();
//添加追踪事件
velocityTracker.addMovement(event);
B、接着在 ACTION_UP 事件中获取当前的速度。注意这里计算的是1000ms时间间隔移动的像素值,假设像素是100,即速度是每秒100像素。另外,手指逆着坐标系的正方向滑动,所产生的速度为负值 ,顺着正反向滑动,所产生的速度为正值。
//获取速度前先计算速度,这里计算的是在1000ms内
velocityTracker .computeCurrentVelocity(1000);
//得到的是1000ms内手指在水平方向从左向右滑过的像素数,即水平速度
float xVelocity = velocityTracker .getXVelocity();
//得到的是1000ms内手指在水平方向从上向下滑过的像素数,垂直速度
float yVelocity = velocityTracker .getYVelocity();
C、最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:
velocityTracker.clear();
velocityTracker.recycle();
A、使用过程:创建一个GestureDetecor对象并实现OnGestureListener接口,根据需要实现单击等方法:
GestureDetector mGestureDetector = new GestureDetector(this);//实例化一个GestureDetector对象
mGestureDetector.setIsLongpressEnabled(false);// 解决长按屏幕后无法拖动的现象
B、接着,接管目标view的onTouchEvent方法,在待监听view的onTouchEvent方法中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
C、然后,就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法了。
建议:如果只是监听滑动操作,建议在onTouchEvent中实现;如果要监听双击这种行为,则使用GestureDetector 。
- scrollBy 是内部调用了 scrollTo 的,它是基于当前位置的相对滑动;而scrollTo是绝对滑动,因此如果利用相同输入参数多次调用scrollTo()方法,由于View初始位置是不变只会出现一次View滚动的效果而不是多次。
- 注意:两者都只能对view内容进行滑动,而不能使view本身滑动。
比如将一个View向右移动100像素,向右,只需要把它的marginLeft参数增大即可,代码见下:
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求重新对View进行measure、layout
- 与scrollTo/scrollBy不同:scrollTo/scrollBy过程是瞬间完成的,非平滑;而Scroller则有过渡滑动的效果。
- 注意:Scoller本身无法让View弹性滑动,它需要和 View 的 computerScroller 方法配合使用。
Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象
private void smoothScrollTo(int dstX, int dstY) {
int scrollX = getScrollX();//View的左边缘到其内容左边缘的距离
int scrollY = getScrollY();//View的上边缘到其内容上边缘的距离
int deltaX = dstX - scrollX;//x方向滑动的位移量
int deltaY = dstY - scrollY;//y方向滑动的位移量
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动
invalidate(); //刷新界面
}
@Override//计算一段时间间隔内偏移的距离,并返回是否滚动结束的标记
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurY());
postInvalidate();//通过不断的重绘不断的调用computeScroll方法
}
}
具体实现:在 MotionEvent.ACTION_UP 事件触发时调用 startScroll 方法->马上调用invalidate/postInvalidate 方法->会请求 View 重绘,导致 View.draw 方法被执行->会调用 View.computeScroll 方法,此方法是空实现,需要自己处理逻辑。具体逻辑是:先判断 computeScrollOffset,若为 true(表示滚动未结束),则执行 scrollTo 方法,它会再次调用 postInvalidate,如此反复执行,直到返回值为 false。
原理:Scroll 的 computeScrollOffset() 根据时间的流逝动态百分比计算一小段时间里View滑动的距离,并得到当前View位置,再通过scrollTo继续滑动。即把一次滑动拆分成无数次小距离滑动从而实现弹性滑动。
动画本身就是一种渐近的过程,故可通过动画来实现弹性滑动。
//在100ms内使得View从原始位置向右平移100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Handler和View的postDelayed方法,也可使用线程的sleep方法。
事件分发的本质:其实就是对 MotionEvent 事件分发的过程。即当一个 MotionEvent 产生了以后,系统需要将这个点击事件传递到一个具体的View上。
点击事件的传递顺序:Activity(Window) -> ViewGroup -> View
其中三个主要方法:
- 事件分发是逐级下发的,目的是将事件传递给一个View。当最后一个View 没有消费事件,这个事件会依次返转,最后回到最高位的Activity,如果这样都没消费的话才抛弃。(责任链)
- 在ViewGroup事件分发中,View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。
- 当一个 View 需要处理事件时,如果设置了 OnTouchListener,那么 OnTouchListener 的 onTouch 方法会回调,如果返回 true,那么 onTouchEvent 方法将不会调用(同时onClick事件是在onTouchEvent中调用)所以三者优先级是onTouch->onTouchEvent->onClick(onClickListener)。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK)
== ENABLED && mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}
含义:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。
方法:需要重写父容器的 onInterceptTouchEvent 方法,在内部做出相应的拦截。
过程:在 onInterceptTouchEvent 方法中,首先在 ACTION_DOWN 这个事件中,父容器必须返回 false,即不拦截 ACTION_DOWN 事件,因为一旦父容器拦截了 ACTION_DOWN,那么后续的 ACTION_MOVE / ACTION_UP 都会直接交给父容器处理;其次是 ACTION_MOVE,根据需求来决定是否要拦截;最后 ACTION_UP 事件,这里必须要返回 false,一旦拦截子View的onClick事件将不会触发。
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.ACTION_MOVE:
if (父容器需要当前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default : break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
含义:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。
方法:需要配合requestDisallowInterceptTouchEvent方法。
过程:父元素需要默认拦截除 ACTION_DOWN 以外的事,这样子元素调用 parent.requestDisallowInterceptTouchEvent(false) 方法时,父元素才能继续拦截需要的事件。(ACTION_DOWN 事件不受 requestDisallowInterceptTouchEvent 方法影响,所以一旦父元素拦截 ACTION_DOWN 事件,那么所有元素都无法传递到子元素去)。
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);//为true表示禁止父容器拦截
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
- measure 过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasureHeight方法来获取到View的宽/高。
- Layout 过程决定了View的四个顶点的坐标和View的实际宽高,完成以后通过getTop、getBottom、getLeft、getRight来拿到四个顶点的位置,并通过getWidth和getHeight方法来拿到View的最终宽高。
- Draw 过程决定了View的显示,只有draw方法完成后View的内容才能呈现在屏幕上。
作用:通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec 决定View的大小。
组成:32 位的 int 值,高 2 位 代表 SpecMode测量模式,低 30 位代表 SpecSize 规格大小。
三种测量模式:
决定因素:值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定。
LayoutParams布局参数:具体数值、match_parent和wrap_content。
从getDefaultSize()中可以看出,直接继承View的自定义View需要重写onMeasure()并设置wrap_content时的自身大小,否则效果相当于macth_parent。
ViewGroup 通常情况下不需要绘制,因为本身没有需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么连 ViewGroup 都不会调用。ViewGroup 会使用dispatchDraw方法来绘制其子View。
优先度onTouch()>onTouchEvent()>onClick()。因此onTouchListener的onTouch()方法会先触发;如果onTouch()返回false才会接着触发onTouchEvent(),同样的,内置诸如onClick()事件的实现等等都基于onTouchEvent();如果onTouch()返回true,这些事件将不会被触发。
SurfaceView是从View基类中派生出来的显示类,它和View的区别有:
- View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新
- View适用于主动更新的情况,而SurfaceView适用于被动更新,如频繁刷新,这是因为如果使用View频繁刷新会阻塞主线程,导致界面卡顿
- SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于需要频繁刷新、刷新时数据处理量很大的页面
invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中需要配合handler使用;而postInvalidate()可在子线程中直接调用。
调用invalidate()只会执行onDraw方法;调用requestLayout()只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。
所以当我们进行View更新时,若仅View的显示内容发生改变,则只需调用invalidate方法;若View宽高和位置发生改变,则调用requestLayout方法;若两者均发生改变,则需先调用requestLayout()再调用invalidate()。
getWidth():得到的是View在父Layout中布局好后的宽度值,如果没有父布局,那么默认的父布局就是整个屏幕。
getMeasuredWidth():得到的是最近一次调用measure()方法测量后得到的是View的宽度,它仅仅用在测量和Layout的计算中。所以此方法得到的是View的内容占据的实际宽度。
总结:
getWidth(): View在设定好布局后整个View的宽度。
getMeasuredWidth(): 对View上的内容进行测量后得到的View内容占据的宽度,前提是你必须在父布局的onLayout()方法或者此View的onDraw()方法里调用measure(0,0);否则你得到的结果和getWidth()得到的结果是一样的。
因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同。
总结一句话:View的measure过程的onMeasure()具有统一实现,而ViewGroup则没有。
原文:https://www.cnblogs.com/awkflf11/p/12085363.html