IT教程 ·

进阶之路 | 巧妙的View之旅

Go1.14发布了,快来围观新的特性啦

媒介

本文已收录到我的Github个人博客,迎接大佬们莅临舍下:

进修清单:

  • View是什么
  • View的位置参数
  • View的触控
  • View的滑动

触及以下各个学问点:

  • View的种种滑动体式格局及其对照
  • 弹性滑动
  • 滑动争执
  • View的动画
  • View的事宜分发机制
  • View的事情道理
  • View的自定义体式格局

一.为何要进修View?

View,是Android中十分重要的一个学问点,是一切控件的基类,只管View不属于四大组件,然则它的作用堪比四大组件,以至重要性大于ContentProviderBroadcast Receivers

ViewGroupView的继承,它的内部包括了一组View。

许多时刻,面临产物司理的种种奇葩的需求,仅仅运用系统供应的控件是不能满足需求的,因而,我们就须要自定义特定的控件,而自定义控件就须要对View系统有肯定程度的明白;有时刻,触及到滑动事宜的自定义View的时刻,难免会涌现林林总总的滑动争执,而要处置惩罚滑动争执的话,还须要对View的事宜分发机制了然于心。

综上,掌握好View这方面的学问,不仅可以让你在一样平常开发中对自定义View的种种场景胸中有数,还可以让你在面试官的重重诘问(ai hu)下游刃有余(xin tai bao zha)。

进阶之路 | 巧妙的View之旅 IT教程 第1张

二.中心学问点归结

2.1 View的位置参数

Q1:Android坐标系是如何的呢?

以屏幕的左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向

进阶之路 | 巧妙的View之旅 IT教程 第2张

Q2:View的位置怎样肯定?

  • 由四个极点肯定,离别对应四个属性:top、left、right、bottom
  • left是左上角的横坐标,left = getLeft()
  • right是右下角的横坐标,right = getRight()
  • top是左上角的纵坐标,top = getTop()
  • bottom是右下角的纵坐标,bottom=getBottom()

注重:这些坐标是相关于父容器而言的,属于相对坐标;假如想要获得相对坐标,须要挪用getRawX(),相对坐标的学问在下文将会细致解说。

进阶之路 | 巧妙的View之旅 IT教程 第3张

因而,View的宽高和坐标关联:

  • width = right - left,可直接经由历程getWidth()获得
  • height = bottom - top,可直接经由历程getHeight()获得

Q3:View偏移量translation

translationXtranslationY是View 左上角相对父容器左上角的偏移量,它们默许值是0。这些参数也是相关于View父容器

进阶之路 | 巧妙的View之旅 IT教程 第4张

  • 存在关联:x = left + translationX,y = top + translationY
  • 因而可知,x和left差别体如今:
  • left是View的初始坐标,在绘制终了后就不会再转变;
  • x是View偏移后的及时坐标,是现实坐标。y和top的区分同理。

须要注重的是,在onCreate()要领里没法猎取到View的坐标参数,这是因为此时View还未入手下手绘制,悉数坐标参数将都是0。

2.2 View的触控

2.2.1 MotionEvent

它是手指触摸屏幕所发生的一系列事宜。典范事宜有:

  • ACTION_DOWN:手指刚打仗屏幕
  • ACTION_MOVE:手指在屏幕上滑动
  • ACTION_UP:手指在屏幕上松开的一霎时

事宜列:从手指打仗屏幕至手指脱离屏幕,这个历程发生的一系列事宜,任何事宜列都是以DOWN事宜入手下手,UP事宜完毕,中心有无数的MOVE事宜

  • 经由历程MotionEvent 对象可以获得触摸事宜的x、y坐标。个中经由历程getX()getY()可猎取相关于当前view(注重:不是父容器)左上角的x、y坐标(相对坐标);
  • 经由历程getRawX()getRawY()可猎取相关于手机屏幕左上角的x,y坐标(相对坐标)。

    详细关联见下图:

进阶之路 | 巧妙的View之旅 IT教程 第5张

2.2.2 TouchSlop

  • 系统所能辨认的被以为是滑动的最小距离。即当手指在屏幕上滑动时,假如两次滑动之间的距离小于这个常量,那末系统就不以为你是在举行滑动操纵。
  • 该常量和装备有关,可用它来推断用户的滑动是不是到达阈值
  • 猎取要领:ViewConfiguration.get(getContext()).getScaledTouchSlop()

2.2.3 VelocityTracker

速率追踪,用于追踪手指在滑动历程当中的速率,包括水温和竖直方向的速率。

运用历程:

  • 在view的onTouchEvent要领中追踪当前单击事宜的速率:
    VelocityTracker velocityTracker = VelocityTracker.obtain();//实例化一个VelocityTracker 对象
    velocityTracker.addMovement(event);//增加追踪事宜
  • ACTION_UP事宜中猎取当前的速率
    velocityTracker .computeCurrentVelocity(1000);//猎取速率前先盘算速率,这里盘算的是在1000ms内
    float xVelocity = velocityTracker .getXVelocity();//获得的是1000ms内手指在程度方向从左向右滑过的像素数,即程度速率
    float yVelocity = velocityTracker .getYVelocity();//获得的是1000ms内手指在程度方向从上向下滑过的像素数,垂直速率

    注重速率方向,这个速率方向和下面的mScrollX的方向相反

  • 当不须要运用它的时刻,须要挪用clear要领来重置并接纳内存
    velocityTracker.clear();
    velocityTracker.recycle();

2.2.4 GestureDetector

手势检测,用于辅佐检测用户的单击、滑动、长按、双击等行动

运用历程:

  • 建立一个GestureDetecor对象并完成OnGestureListener接口,依据须要完成单击等要领
    GestureDetector mGestureDetector = new GestureDetector(this);//实例化一个GestureDetector对象
    mGestureDetector.setIsLongpressEnabled(false);// 处置惩罚长按屏幕后没法拖动的征象
  • 接受目标view的onTouchEvent要领,在待监听view的onTouchEvent要领中增加以下完成
    boolean consume = mGestureDetector.onTouchEvent(event);
    return consume;
  • 有挑选的完成OnGestureListener和OnDoubleTapListener中的要领

发起:假如只是监听滑动操纵,发起在onTouchEvent中完成;假如要监听双击这类行动,则运用GestureDetector

2.3 View的滑动

2.3.1 View滑动的七种体式格局

1. scrollTo/scollBy
  • 区分:scrollBy是内部挪用了scrollTo的,它是基于当前位置的相对滑动;而scrollTo相对滑动,因而假如应用雷同输入参数屡次挪用scrollTo()要领,因为View初始位置是稳定只会涌现一次View转动的结果而不是屡次。
  • 注重:二者都只能对view内容举行滑动,而不能使view本身滑动。
  • 方向:手指从右向左滑动,mScrollX为正值,反之为负值;手指从下往上滑动,mScrollY为正值,反之为负值。(更直观感觉:检察下一张照片或许检察长图时手指滑动方向为正)
  • 滑动范例:非弹性滑动

进阶之路 | 巧妙的View之旅 IT教程 第6张

2. LayoutParams
  • 道理:经由历程转变View的LayoutParams使得View从新规划:比方将一个View向右挪动100像素,向右,只须要把它的marginLeft参数增大即可
  • 滑动范例:非弹性滑动
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求从新对View举行measure、layout
3. 动画
  • 动画分为View动画和属性动画,View动画又分为帧动画和补间动画
  • 假如运用属性动画的话,为了可以兼容3.0以下版本,须要采纳开源动画库nineoldandroids。
  • 属于弹性滑动

进阶之路 | 巧妙的View之旅 IT教程 第7张

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

想要相识动画细致内容的读者,可以看一下笔者这篇文章:

4. layout()
  • 基本思想:记下触摸点的坐标挪动今后,记下挪动后的坐标算出偏移量
  • 运用体式格局:在onTouchEvent中猎取到手指的横纵坐标,在ACTION_DOWN中存储上次的x,在ACTION_MOVE中盘算挪动的距离,末了挪用layout要领从新安排View
public boolean onTouchEvent(MotionEvent event) {
        //猎取到手指处的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //lastX是存储上一次的x
                lastX = x;
                lastY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                //盘算挪动的距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //挪用layout要领来从新安排它的位置,左上右下
               layout(getLeft()+offsetX, getTop()+offsetY,
                       getRight()+offsetX , getBottom()+offsetY);
                break;
                return true;
        }
5. offsetLeftAndRight()offsetTopAndBottom()

运用体式格局相似于layout(),将 layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX , getBottom()+offsetY)换成offsetLeftAndRight(offsetX)offsetTopAndBottom(offsetY)即可

           // 对left和right举行偏移
           offsetLeftAndRight(offsetX);
            //对top和bottom举行偏移
           offsetTopAndBottom(offsetY);
6. Scroller
  • 与scrollTo/scrollBy差别:scrollTo/scrollBy历程是霎时完成的,非腻滑;而Scroller则有过渡滑动的结果
  • 注重:Scoller本身没法让View弹性滑动,它须要和View的computeScroll要领合营运用。
  • 道理:Scoller的computeScrollOffset()依据时候的流逝动态盘算一小段时候里View滑动的距离,并获得当前View位置,再经由历程scrollTo继承滑动。即把一次滑动拆分红无数次小距离滑动从而完成弹性滑动。

Scroller习用代码:

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要领
  }
}

startScroll()的源码:

只是举行前期的准备事情,并没有举行现实的滑动操纵,而是经由历程后续invalidate()要领去做滑动行动。

public void startScroll(int startX,int startY,int dx,int dy,int duration){
  mMode = SCROLL_MODE;
  mFinished = false;
  mDuration = duration;//滑动时候
  mStartTime = AnimationUtils.currentAminationTimeMills();//入手下手时候
  mStartX = startX;//滑动出发点
  mStartY = startY;//滑动出发点
  mFinalX = startX + dx;//滑动尽头
  mFinalY = startY + dy;//滑动尽头
  mDeltaX = dx;//滑动距离
  mDeltaY = dy;//滑动距离
  mDurationReciprocal = 1.0f / (float)mDuration;
 }

进阶之路 | 巧妙的View之旅 IT教程 第8张

7. 延时战略
  • 经由历程发送一系列延时信息从而到达一种渐近式的结果,详细可以经由历程Handler/ViewpostDelayed,也可运用线程的sleep要领。
  • 瑕玷:没法准确地定时;缘由:系统的音讯调理也须要时候

2.3.2 滑动争执

Q1:发生缘由

平常状况下,在一个界面里存在表里两层可同时滑动的状况时,会涌现滑动争执征象。

Q2:涌现的场景:

  • 场景一:外部滑动和内部滑动方向不一致:如ViewPager嵌套ListView(现实这么用没问题,因为ViewPager内部已处置惩罚过)。
  • 场景二:外部滑动方向和内部滑动方向一致:如ScrollView嵌套ListView。

读者假如想要相识涌现缘由以及处置惩罚体式格局,笔者引荐一篇文章:

  • 场景三:上面两种状况的嵌套

Q3:处置惩罚划定规矩

  • 对场景一:当用户摆布/高低滑动时让外部View阻拦点击事宜,当用户高低/摆布滑动时让内部View阻拦点击事宜。即依据滑动的方向推断谁来阻拦事宜。关于推断是高低滑动照样摆布滑动,可依据滑动的距离或许滑动的角度去推断。
  • 对场景二:平常从营业上找突破点。即依据营业需求,划定什么时候让外部View阻拦事宜什么时候由内部View阻拦事宜。
  • 对场景三:相对庞杂,可一样依据需求在营业上找到突破点。

Q4:处置惩罚体式格局

这里的onInterceptTouchEventdispatchTouchEventrequestDisallowInterceptTouchEvent等要领在View的事宜分发机制会细致申明

A1:外部阻拦法

  • 寄义:指点击事宜都先经由父容器的阻拦处置惩罚,假如父容器须要此事宜就阻拦,不然就不阻拦。
  • 要领:须要重写父容器的onInterceptTouchEvent要领,在内部做出响应的阻拦。
//重写父容器的阻拦要领
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://关于ACTION_DOWN事宜必需返回false,一旦阻拦后续事宜将不能通报给子View
         intercepted = false;
         break;
      case MotionEvent.ACTION_MOVE://关于ACTION_MOVE事宜依据须要决议是不是阻拦
         if (父容器须要当前事宜) {
             intercepted = true;
         } else {
             intercepted = flase;
         }
         break;
   }
      case MotionEvent.ACTION_UP://关于ACTION_UP事宜必需返回false,一旦阻拦子View的onClick事宜将不会触发
         intercepted = false;
         break;
      default : break;
   }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
   }

A2:内部阻拦法

  • 寄义:指父容器不阻拦任何事宜,而将一切的事宜都通报给子容器,假如子容器须要此事宜就直接斲丧,不然就交由父容器举行处置惩罚。
  • 要领:须要合营requestDisallowInterceptTouchEvent要领。重写子ViewdispatchTouchEvent()
    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);//为fasle示意许可父容器阻拦
             }
             break;
          case MotionEvent.ACTION_UP:
             break;
          default :
             break;        
     }
    
      mLastX = x;
      mLastY = y;
      return super.dispatchTouchEvent(event);
    }

    除子容器须要做处置惩罚外,父容器也要默许阻拦除了ACTION_DOWN之外的其他事宜,如许当子容器挪用parent.requestDisallowInterceptTouchEvent(false)要领时,父元素才继承阻拦所需的事宜。

    因而,父View须要重写onInterceptTouchEvent()

    public boolean onInterceptTouchEvent (MotionEvent event) {
     int action = event.getAction();
     if(action == MotionEvent.ACTION_DOWN) {
         return false;
     } else {
         return true;
     }
    }

内部阻拦法请求父容器不能阻拦ACTION_DOWN的缘由:

因为该事宜并不受FLAG_DISALLOW_INTERCEPT(由requestDisallowInterceptTouchEvent要领设置)标记位掌握,一旦ACTION_DOWN事宜到来,该标记位会被重置。所以一旦父容器阻拦了该事宜,那末一切的事宜都不会通报给子View,内部阻拦法也就失效了。

2.4 View的事宜分发机制

读者看完本篇对事宜分发机制还有些隐约的话,笔者墙裂引荐一篇浅显易懂的文章:

Q1:相识setContentView()

我们将从源码的角度,一步步带人人深切setContentView()的实质,为背面事宜分发机制的相识打好基本

进阶之路 | 巧妙的View之旅 IT教程 第9张

因而,我们可以获得Activity的构成,以下图所示

进阶之路 | 巧妙的View之旅 IT教程 第10张

Q2:事宜分发实质是什么:

就是对MotionEvent事宜分发的历程。即当一个MotionEvent发生了今后,系统须要将这个点击事宜通报到一个详细的View上。(关于MotionEvent引见见本篇2.2.1)

Q3:事宜分发须要的重要要领是什么

  • dispatchTouchEvent:举行事宜的分发(通报)。返回值是 boolean 范例,受当前onTouchEvent下级viewdispatchTouchEvent影响
  • onInterceptTouchEvent:对事宜举行阻拦。该要领只在ViewGroup中有,View(不包括 ViewGroup)是没有的。假如一旦阻拦,则实行ViewGrouponTouchEvent,在ViewGroup中处置惩罚事宜,而不接着分发给View,且只挪用一次,所以背面的事宜都邑交给ViewGroup处置惩罚。
  • onTouchEvent:举行事宜处置惩罚

进阶之路 | 巧妙的View之旅 IT教程 第11张

  • 事宜分发是逐级下发的,目标是将事宜通报给一个View。
  • ViewGroup一旦阻拦事宜,就不往下分发,同时挪用onTouchEvent处置惩罚事宜。

2.5 View的事情道理

2.5.1 View事情流程

measure丈量->layout规划->draw绘制

  • measure肯定View的丈量宽高
  • layout肯定View的终究宽高四个极点的位置
  • draw将View 绘制到屏幕
  • 对应onMeasure()onLayout()onDraw()三个要领。

详细历程:

  • ViewRoot对应于ViewRootImpl类,它是衔接WindowManagerDecorView的纽带
  • View的绘制流程是从ViewRoot.performTraversals入手下手。
  • performTraversals()顺次挪用performMeasure()performLayout()performDraw()三个要领,完成顶级 View的绘制。
  • 个中,performMeasure()会挪用measure()measure()中又挪用onMeasure(),完成对其一切子元素的measure历程,如许就完成了一次measure历程;接着子元素会重复父容器的measure历程,云云重复至完成全部View树的遍历。layout和draw同理。历程图以下:

进阶之路 | 巧妙的View之旅 IT教程 第12张

2.5.2 measure

先来明白MeasureSpec

  • 作用:经由历程宽丈量值widthMeasureSpec和高丈量值heightMeasureSpec决议View的大小
  • 构成:一个32位int值,高2位代表SpecMode(丈量形式),低30位代表SpecSize( 某种丈量形式下的规格大小)。
  • 三种形式:

    a.UNSPECIFIED: 父容器不对View有任何限定,要多大有多大。常用于系统内部。

    b.EXACTLY(准确形式): 父视图为子视图指定一个确实的尺寸SpecSize。对应LayoutParams中的match_parent详细数值

    c.AT_MOST(最大形式): 父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content

  • 决议因素:由子View的规划参数LayoutParams父容器MeasureSpec值配合决议。

进阶之路 | 巧妙的View之旅 IT教程 第13张

如今,离别议论两种measure

  • View的measure:只要一个原始的View,经由历程measure()即可完成丈量。

    进阶之路 | 巧妙的View之旅 IT教程 第6张

getDefaultSize()中可以看出,直接继承View的自定义View须要重写onMeasure()并设置wrap_content时的本身大小,不然结果相当于macth_parent。处置惩罚上述问题的典范代码:

要领一:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //剖析形式,依据差别的形式来设置
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }

要领二:

  • 道理实在和要领一相似,就是resolveSize封装了要领一的一系列操纵
  • 想探讨resolveSize源码的可以看一下这篇文章:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        int width=resolveSize(mWidth, widthMeasureSpec);
        int height=resolveSize(mHeight, heightMeasureSpec);
        setMeasuredDimension(width,height);
    }
  • ViewGroup的measure:除了完成ViewGroup本身的丈量外,还会遍历去挪用一切子元素的measure要领。

ViewGroup中没有重写onMeasure(),而是供应measureChildren()

进阶之路 | 巧妙的View之旅 IT教程 第15张

假如读者对onMeasure的细致重写例子感兴趣的话,笔者引荐一篇文章:

2.5.3 layout

  • 肯定View的终究宽高和四个极点的位置

大抵流程:从顶级View入手下手顺次挪用layout(),个中子View的layout()会挪用setFrame()来设定本身的四个极点(mLeft、mRight、mTop、mBottom),接着挪用onLayout()来肯定其坐标,注重该要领是空要领,因为差别的ViewGroup对其子View的规划是不雷同的。

进阶之路 | 巧妙的View之旅 IT教程 第16张

假如读者对onLayout()的细致重写例子感兴趣的话,笔者引荐一篇文章:

2.5.4 draw

引荐浏览

  • 绘制到屏幕

绘制次序:

  • 绘制背景:background.draw(canvas)
  • 绘制本身:onDraw(canvas)
  • 绘制children:dispatchDraw(canvas)
  • 绘制装潢:onDrawScrollBars(canvas)

进阶之路 | 巧妙的View之旅 IT教程 第17张

注重:View有一个迥殊的要领setWillNotDraw(),该要领用于设置 WILL_NOT_DRAW 标记位(其作用是当一个View不须要绘制内容时,系统可举行响应优化)。默许状况下View是没有这个优化标志的(设为true)。

2.6 自定义View

假如想相识自定义View实例的读者,笔者引荐一篇文章:

Q1:自定义View的范例有哪些

进阶之路 | 巧妙的View之旅 IT教程 第18张

迥殊提示

三.教室小测试

祝贺你!已看完了前面的文章,相信你对View已有肯定深度的相识,下面,举行一下教室小测试,考证一下本身的进修效果吧!

Q1:View的丈量宽高和终究宽高有什么区分

这个问题详细为ViewgetMeasuredWidthgetWidth有什么区分?

  • 答案发表:

    View默许完成中,丈量宽高和终究宽高相称,然则丈量宽高的赋值机遇比终究宽高的赋值机遇轻微早一点,丈量宽高构成于measure历程,终究宽高构成于View的layout历程。

Q2:什么状况下丈量宽高和终究宽高不一致呢

  • 重写了View的layout要领
    public void layout(int l,int t,int r, int b){
      super.layout(l,t,r+100,b+100);
    }
  • 在某些状况下,View须要屡次measure才肯定本身的丈量宽高,在前频频的丈量历程当中,得出的丈量宽高有大概和终究宽高不一致,但终究二者照样一致的。

假如文章对您有一点协助的话,愿望您能点一下赞,您的点赞,是我行进的动力

本文参考链接:

  • 《Android 进阶之光》
  • 《Android 开发艺术探究》

腾讯抢金达人项目中的前后端协作

参与评论