四、界面编程(一) View的基础知识及架构详解

来源:互联网 发布:知乎 女朋友是拉拉 编辑:程序博客网 时间:2024/05/21 06:31

  • View的基本认识及布局架构
  • View的常用概念
    • View的位置参数
    • MotionEvent和TouchSlop
      • MotionEvent
      • TouchSlop
    • VelocityTackerGestureDetector和Scroller
      • VelocityTacker速度追踪
      • GestureDetector手势检测
      • Scroller弹性滑动对象
  • View的测量与绘制浅析
    • View的测量
    • View的绘制
  • ViewGroup的测量与绘制浅析
    • ViewGroup的测量
    • ViewGroup的绘制

  View虽然不属于四大组件,但是它的作用堪比四大组件,甚至比Receiver和Provider的重要性都更大。本文将介绍View的基础知识、常用概念、测量绘制简介等内容,通过这些内容的理解,可以更深入的了解View,也为更复杂的内容做下铺垫。

View的基本认识及布局架构

  首先,什么是View呢?View是一种界面层控件的抽象,是Android中所有控件的基类,不论是简单的Button和TextView还是复杂的RelativeLayout和ListView,它们共同的基类都是View。

  Android中的每个控件都会在界面中占得一块矩形区域,通常我们会将控件分为两类,即ViewGroup控件和View控件。ViewGroup控件,顾名思义,指的是控件组,也就是一组控件,意味着ViewGroup内部包含了许多个View控件。在Android的设计中,ViewGroup继承了View,也就是意味着View本身就可以是单个控件或者是由多个控件组成的一组控件。ViewGroup控件作为父控件可以包含多个View控件,并管理其包含的View控件。通过ViewGroup,整个界面上的控件形成了一个树形结构,即控件树,上层控件负责下层子控件的测量与绘制,并传递交互事件。通常使用的findViewById就是在控件树中以树的深度优先遍历来查找对应元素。在每棵控件树的顶部,都拥有一个ViewParent对象,这就是整棵树的控制核心,所有的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。View树结构如下图:
这里写图片描述

  通常情况下,在Activity中使用了setContentView( )方法来设置一个布局,调用该方法之后,布局内容才真正地显示出来。那么setContentView方法做了什么呢?首先,来看一下Android界面的架构图:
这里写图片描述

  在Android界面的架构图中,我们可以看到,每个Activity都包含一个Window对象,在Android中Window对象通常由PhoneWindow来实现。PhoneWindow将一个DecorView设置为整个应用窗口的根View。DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。通过以上过程,我们可以建立一个标准视图树,如下图:
这里写图片描述

  视图树的第二层装载了一个LinearLayout,作为ViewGroup,这一层的布局结构会根据对应的参数设置不同的布局,如最常用的布局——上面显示TitleBar下面是Content这样的布局。
  用户可以通过设置requestWindowFeature(Window.FEATURE_NO_TITLE)来设置全屏显示,视图树中的布局就就只有Content了,这就解释了为什么调用requestWindowFeature()方法一定要在调用setContentView()方法之前才能生效的原因了。
  在代码中,当程序在onCreate()方法中调用setContentView()方法后,ActivityManagerService会回调onResume()方法,此时系统才会把整个DecorView添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制。

View的常用概念

View的位置参数

  View的位置主要由它的四个顶点来定义,分别对应与View的四个属性:top、left、right、bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标,这些坐标都是相对于View的父容器来说的,它是一种相对坐标。在Android中,x轴和y轴的正方向分别是向右和向下。所以View的坐标和父容器关系如下所示:
这里写图片描述

  根据图中所示,View内有四个成员变量,分表代表了Top、Left、Right、Bottm的值,所以我们可以利用View的get方法获取这些值:

int left = View.getLeft();int top = View.getTop();int right = View.getRight();int bottom = View.getBottom()

我们也可以很容易的计算出View的宽高和坐标的关系:

int width = right - left;int height = bottom - top;

  从Android3.0开始,View增加了几个额外的参数:x、y、translationX、translationY。其中x和y是指View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。View在发生平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、transactionX和transactionY这四个参数。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值为0,和View的四个基本位置参数一样,View也为它们提供了get/set方法,这几个参数之间的换算关系如下:

x = left + transactionX;y = right + transactionY;

下面的图可以更好的理解这些额外参数的概念:
这里写图片描述

MotionEvent和TouchSlop

MotionEvent

在手指接触屏幕后会产生一系列事件,其中典型的事件类型有以下几个:

ACTION_DOWN ------ 手指刚接触屏幕ACTION_MOVE ------ 手指在屏幕上移动ACTION_UP   ------ 手指从屏幕上松开的一瞬间

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,例如

点击屏幕后手指立刻松开,事件序列为DOWN-->UP点击屏幕滑动一会再松开,事件序列为DOWN --> MOVE --> MOVE ...-->MOVE-->UP

  系统提供了两组方法获取点击事件发生的x和y的坐标:getX()/getY()获取相对于当前view的左上角的坐标,getRawX()/getRawY()获取相对于屏幕左上角的坐标。

TouchSlop

  TouchSlop是系统所能识别的被认为滑动的最小距离,是系统认为滑动和点击事件的临界点,当然TouchSlop在各家手机系统默认值是不同的,我们可以通过:ViewConfiguration.get(getContext().getScaledTouchSlop()) 获取系统的滑动常量来,判断此时是否属于滑动事件。这个常量可以用于滑动事件的过滤,提升用户体验。

VelocityTacker,GestureDetector和Scroller

VelocityTacker(速度追踪)

  速度追踪,用于追踪手指在滑动过程中的速度,包括水平和数值方向的速度,使用方法是在View的OnTouchEvent方法中获取

//获得速度追踪对象VelocityTracker velocity = VelocityTracker.obtain();velocity.addMovement(event);//计算速度 并获取计算值velocity.computeCurrentVelocity(1000); //设定一个时间间隔值(单位是毫秒),并计算速度float xVelocity = velocity.getXVelocity(); //获取水平方向的速度,获取速度前必须先计算速度float yVelocity = velocity.getYVelocity(); //获取垂直方向的速度

  这里的速度是指在设定时间段内水平方向从左向右滑动的像素数,即1秒内从左向右滑动了100像素,那么水平速度就是100像素/秒。速度可以为负数,即当手指从右向左滑动时,水平方向的速度即为负数。

  最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:

VelocityTracker.clear();VelocityTracker.recycle();

GestureDetector(手势检测)

手势检测,用于辅助检测用户的单击, 滑动, 长按, 双击等行为。使用方法如下:
第一步:创建GestureDetector对象并实现OnGestureDetector接口:

GestureDetector mGestureDetector = new GestureDetector(this);// 解决长按屏幕后无法拖动的效果mGestureDetector.setIsLongpressEnabled(false);

第二步:接管目标View的onTouchEvent()方法. 在onTouchEvent()方法中,在待监听的View的onTouchEvent方法中添加如下实现

boolean consume = mGestureDetector.onTouchEvent(event);return consume;

第三步:根据需求可以选择性的实现OnGestureListener和OnDoubleTapListener接口

这两个接口的方法介绍如下表
这里写图片描述
如果只是监听滑动相关的,可以再onTouchEvent中实现,如果是要监听双击行为的话,那么可以使用GestureDetector

Scroller(弹性滑动对象)

  弹性滑动对象,用于实现View的弹性滑动。在开发中, 当需要把View从一个点移动到另一个点的时候, 如果使用scrollTo/scrollBy进行滑动时, 都是瞬间完成。 没有过度动画, 给用户感觉很生硬。 使用Scroller 可以实现有过渡的滑动。Scroller本身无法让View弹性滑动, 需要和View的computerScroll进行配合使用。

scroller的两个重要方法:
mScroller.startScroll(0, 0, 0, 0, 1000) 前面的两个参数是起始坐标x,y,中间两个参数是对应的偏移量,最后一个参数是执行时间。
mScroller.computeScrollOffset() 判断是否完成滚动,这个函数会一直返回false,直到滚动完毕返回true。

获取Scroller携带的位置数据:
mScroller.getCurrX() //获取mScroller当前水平滚动的位置
mScroller.getCurrY() //获取mScroller当前竖直滚动的位置
mScroller.getFinalX() //获取mScroller最终停止的水平位置
mScroller.getFinalY() //获取mScroller最终停止的竖直位置
mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置
mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置

使用方法:
  ViewGroup中有个computeScroll方法,ontouch或invalidate()或postInvalidate()都会导致这个方法的执行,所以我们可以手动执行ViewGroup方法,同时再computeScroll中执行postInvalidate(),这就会形成一个循环,我们在这个循环中调用ViewGroup的scrollTo方法更新位置信息,同时使用mScroller.computeScrollOffset()方法监听滚动是否完毕。

public class MyView extends LinearLayout {        private boolean s1 = true;        Scroller mScroller = null;        public MyView(Context context, AttributeSet attrs) {            super(context, attrs);            mScroller = new Scroller(context);        }        @Override        public void computeScroll() {            if (mScroller.computeScrollOffset()) {                scrollTo(mScroller.getCurrX(), 0);                postInvalidate();            }        }        //缓慢滑动到指定位置,需要手动执行        public void beginScroll() {            if (!s1) {                mScroller.startScroll(0, 0, 0, 0, 1000);                s1 = true;            } else {        //1000ms内滑动500像素,效果就是慢慢滑动                mScroller.startScroll(0, 0, 500, 0, 1000);                s1 = false;            }            invalidate();        }    }

View的测量与绘制浅析

View的测量

  现实生活中,如果我们要去画一个图形,就必须知道它的大小和位置。同样,Android系统在绘制View前,也必须对View进行测量,告诉系统该画一个多大的View。这个过程在onMeasure()方法中进行。Android系统提供了一个类——MeasureSpec类,通过它来帮助我们测量View。MeasureSpec是一个32位的int值,其中高2位为测量模式,低30位为测量的大小。

测量模式可以分为以下三种:
(1)EXACTLY
  即精确模式,将控件的layout_width属性或layout_height属性指定为具体数值时,比如android:layout_width = “100dp”,或者指定为match_parent属性时(占据父View的大小),系统使用EXACTLY模式。
(2)AT_MOST
  即最大值模式,当控件的layout_width属性或layout_height属性指定为wrap_content时,控件大小一般随着控件的子控件或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
(3)UNSPECIFIED
  这个属性比较奇怪——它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。

  通过MeasureSpec这一个类,我们就获取了View的测量模式和View想要绘制的大小。有了这些信息,我们就可以控制View最后显示的大小。

View测量的实例:
第一步:重写onMeasure()
默认的onMeasure方法

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {   super.onMeasure(widthMeasureSpec, heightMeasureSpec);}

  通过查看super.onMeasure()方法,可以发现,系统最终会调用setMeasuredDimension(int widthMeasureSpec, int widthMeasureSpec)方法将测量后的宽高值设置进去,从而完成测量工作。所以重写onMeasure()方法后,最终要做的就是把测量后的宽高值作为参数设置给setMeasuredDimension()方法。
通过上面的分析,重写的onMeasure()方法代码如下:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));}

  在onMeasure()方法中,我们调用自定义的measureWidth()方法和measureHeight()方法分别对宽高重新定义,参数则是宽和高的MeasureSpec对象,MeasureSpec对象中包含了测量模式和测量值的大小,下面的步骤即是获取所需参数方法的实现

第二步:从MeasureSpec对象中提取出具体的测量模式和大小

int specMode = MeasureSpec.getMode(measureSpec); // 取出测量模式int specSize = MeasureSpec.getSize(measureSpec); // 取出测量大小

第三步:通过判断测量模式,给出不同的测量值
  当specMode为EXACTLY时,直接使用指定的specSize即可;当specMode为其它两种模式时,需要给一个默认的大小。如果指定wrap_content属性,即AT_MOST模式,则需要用我们指定大小与specSize较小值做为最后的测量值。
measureWidth()方法如下所示,这段代码基本可以作为模板代码:

private int measureWidth(int measureSpec) {        int result = size;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        if(specMode == MeasureSpec.EXACTLY) {            result = specSize;        } else {            result = 200; // 不是精确值模式时默认值为200            if(specMode == MeasureSpec.AT_MOST) {                result = Math.min(result, specSize);            }        }        return result;    }

measureHeight()方法与masureWidth()基本一致,通过这两个方法就可以完成对于宽高值的自定义

View的绘制

  View的绘制过程很重要,理解也会较为困难,将在下一篇文章中详细介绍

ViewGroup的测量与绘制浅析

ViewGroup的测量

  ViewGroup会管理其子View,其中有一个管理项目就是View的限时大小。当ViewGroup的大小为wrap_content时,ViewGroup就需要对子View进行遍历,以便获取所有子View的大小,从而决定自己的大小。而在其他模式下则会通过具体的数值来设置自身的大小。

ViewGroup的绘制

  ViewGroup通常情况下不需要绘制,因为本身就没有什么需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么ViewGroup的onDraw( )方法都不会被调用。但是ViewGroup会使用dispatchDraw方法来绘制其子View,过程是遍历所有的子View,并调用子View的绘制方法来完成绘制工作。

原创粉丝点击