Android学习笔记之View

2018-10-22  本文已影响0人  sssssss_
导图

一、View事件体系

1.什么是 View 和 View的位置坐标

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的区别同理。

2.MotionEvent和TouchTop

MotionEvent点击view位置示意图

通过MotionEvent 对象可以得到触摸位置的x、y坐标。其中通过getX()、getY()可获取相对于当前view左上角的x、y坐标;通过getRawX()、getRawY()可获取相对于手机屏幕左上角的x,y坐标。


该常量和设备有关,可用它来判断用户的滑动是否达到阈值,获取方法:
ViewConfiguration.get(getContext()).getScaledTouchSlop()。

3.VelocityTracker 和 GestureDetector

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 。


二、View的滑动

滑动方式1:通过View本身提供的scrollTo/scrollBy方法

  • scrollBy 是内部调用了 scrollTo 的,它是基于当前位置的相对滑动;而scrollTo是绝对滑动,因此如果利用相同输入参数多次调用scrollTo()方法,由于View初始位置是不变只会出现一次View滚动的效果而不是多次。
  • 注意:两者都只能对view内容进行滑动,而不能使view本身滑动。
image

滑动方式2:通过动画给View施加平移效果

滑动方式3:通过改变View的LayoutParams使得View重新布局

比如将一个View向右移动100像素,向右,只需要把它的marginLeft参数增大即可,代码见下:

MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求重新对View进行measure、layout

三种方法比较


三、弹性滑动

弹性滑动方式1:使用 Scroller

  • 与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

image

原理:Scroll 的 computeScrollOffset() 根据时间的流逝动态百分比计算一小段时间里View滑动的距离,并得到当前View位置,再通过scrollTo继续滑动。即把一次滑动拆分成无数次小距离滑动从而实现弹性滑动。

弹性滑动方式2:通过动画

动画本身就是一种渐近的过程,故可通过动画来实现弹性滑动。

//在100ms内使得View从原始位置向右平移100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

弹性滑动方式3:使用延时策略

通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过HandlerView的postDelayed方法,也可使用线程的sleep方法。


四、View的事件分发机制

事件分发流程
  1. 事件分发是逐级下发的,目的是将事件传递给一个View。当最后一个View 没有消费事件,这个事件会依次返转,最后回到最高位的Activity,如果这样都没消费的话才抛弃。(责任链)
  2. 在ViewGroup事件分发中,View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。
  3. 当一个 View 需要处理事件时,如果设置了 OnTouchListener,那么 OnTouchListener 的 onTouch 方法会回调,如果返回 true,那么 onTouchEvent 方法将不会调用(同时onClick事件是在onTouchEvent中调用)所以三者优先级是onTouch->onTouchEvent->onClick(onClickListener)。

View的事件分发机制

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) 
          == ENABLED && mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

ViewGroup的事件分发机制

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else{
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

五、View滑动冲突

1.常见的滑动冲突场景

  1. 外部滑动方向与内部滑动方向不一致(左右滑动:Fragment,上下滑动: ListView)。
  2. 外部滑动方向与内部滑动方向一致,(ScrollView 中包含 ListView );
  3. 上面两种情况的嵌套;

2.滑动冲突的处理规则

  1. 滑动路径和水平方向所形成的夹角
  2. 水平方向和竖直方向上的距离差
  3. 水平和竖直方向的速度差
  4. 从业务的需求上得出相应的处理规则。

3.滑动冲突的解决方式

第一种:外部拦截法

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;
   }

第二种:内部拦截法

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);
}

六、View工作原理

1.View的绘制流程

  1. 整个 View 树的绘图流程是在 ViewRoot 类的 performTraversals() 方法展开的。
  2. performTraversals() 依次调用 performMeasure()performLayout()performDraw()三个方法,分别完成顶级View的绘制。
  3. 其中,performMeasure() 会调用 measure(),measure() 中又调用onMeasure(),实现对其所有子元素的 measure 过程,这样就完成了一次 measure 过程;接着子元素会重复父容器的 measure 过程,如此反复至完成整个 View 树的遍历。
  4. performLayout 和 performDraw 的传递流程也是类似,唯一不同的是 performDraw 都传递过程是用 draw 方法中的 dispatchDraw 来实现。
  • measure 过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidthgetMeasureHeight方法来获取到View的宽/高
  • Layout 过程决定了View的四个顶点的坐标和View的实际宽高,完成以后通过getTop、getBottom、getLeft、getRight来拿到四个顶点的位置,并通过getWidth和getHeight方法来拿到View的最终宽高。
  • Draw 过程决定了View的显示,只有draw方法完成后View的内容才能呈现在屏幕上。
image

2.measure方法

2.1 MeasureSpec

2.2 measure测量过程图

image

从getDefaultSize()中可以看出,直接继承View的自定义View需要重写onMeasure()并设置wrap_content时的自身大小,否则效果相当于macth_parent。

image image

3. layout方法

image image

4. draw方法

ViewGroup 通常情况下不需要绘制,因为本身没有需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么连 ViewGroup 都不会调用。ViewGroup 会使用dispatchDraw方法来绘制其子View。


七、自定义View

1. 自定义 View 的分类

2. 自定义 View 须知

3. 自定义 View 的思想

4. 为什么自定义控件

5.如何自定义控件

  1. 自定义属性的声明与获取。
  2. 测量onMeasure。
  3. 布局onLayout(viewgroup)
  4. 绘制onDraw
  5. onTouchEvent。
  6. onInterceptTouchEvent(ViewGroup)。

6. 自定义属性声明与获取

  1. 分析需要的自定义属性。
  2. 在res/values/attrs.xml定义声明
  3. 在layout.xml方法中进行使用。
  4. 在View的构造方法中进行获取。

7. 自定义View的三要素

  1. Canvas:画布,用于绘制View所要显示的内容,一般来自onDraw()函数的传入。
  2. Paint :画笔,用于绘制View所需要绘制的内容,相当于笔,在其内部可以设置颜色,粗细,是否实心等信息,一般通过new的方式获取该类对象;
  3. Point:点,用于确定View所需要绘制的内容大小及位置等相关信息,当然这里的Point不具有实际意义,也有可能是线段,矩形等,只不过大多数是通过点的组合关系确定而已;

问题1:onTouch()、onTouchEvent()和onClick()关系?

优先度onTouch()>onTouchEvent()>onClick()。因此onTouchListener的onTouch()方法会先触发;如果onTouch()返回false才会接着触发onTouchEvent(),同样的,内置诸如onClick()事件的实现等等都基于onTouchEvent();如果onTouch()返回true,这些事件将不会被触发。

问题2:SurfaceView和View的区别?

SurfaceView是从View基类中派生出来的显示类,它和View的区别有:

  • View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新
  • View适用于主动更新的情况,而SurfaceView适用于被动更新,如频繁刷新,这是因为如果使用View频繁刷新会阻塞主线程,导致界面卡顿
  • SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于需要频繁刷新、刷新时数据处理量很大的页面

问题3:invalidate()和postInvalidate()的区别?

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中需要配合handler使用;而postInvalidate()可在子线程中直接调用。

问题4: requestLayout()和invalidate()的区别?

  1. 调用invalidate()只会执行onDraw方法;调用requestLayout()只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。

  2. 所以当我们进行View更新时,若仅View的显示内容发生改变,则只需调用invalidate方法;若View宽高和位置发生改变,则调用requestLayout方法;若两者均发生改变,则需先调用requestLayout()再调用invalidate()。

View的生命周期

问题5:Android中真实宽高getWidth和getMeasuredWidth的区别:哪个计算的是真实的宽?

getWidth():得到的是View在父Layout中布局好后的宽度值,如果没有父布局,那么默认的父布局就是整个屏幕。
getMeasuredWidth():得到的是最近一次调用measure()方法测量后得到的是View的宽度,它仅仅用在测量和Layout的计算中。所以此方法得到的是View的内容占据的实际宽度。
总结:
getWidth(): View在设定好布局后整个View的宽度。
getMeasuredWidth(): 对View上的内容进行测量后得到的View内容占据的宽度,前提是你必须在父布局的onLayout()方法或者此View的onDraw()方法里调用measure(0,0);否则你得到的结果和getWidth()得到的结果是一样的。

问题6:为什么Viewgroup的Measure过程和View的过程不一样,还要自己重写onMeasure()方法?

因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同。

总结一句话:View的measure过程的onMeasure()具有统一实现,而ViewGroup则没有。

本文参考资料:

上一篇 下一篇

猜你喜欢

热点阅读