安卓开发笔记 之 View 视图

发布于 2024-09-12 14:37:57 字数 10160 浏览 42 评论 0

基本概念

  • 每个 Activity 都有一个 Window(接口),通常是由 PhoneWindow 类来实现的。 PhoneWindow 将 DecorView 作为整个应用窗口的根 View,DecorView 将屏幕分成两部分:TitleView 和 ContentView。所有 view 的操作、绘制都由 ViewRootImpl 完成。
  • setContentView() 做了什么?
  • ViewGroup extends View;
  • view 的位置参数:top、left、right、bottom,分别对应 View 的左上角和右下角相对于父容器的横纵坐标值。
  • 从 Android 3.0 开始,view 增加了 x、y、translationX、translationY 四个参数,这几个参数也是相对于父容器的坐标。x 和 y 是左上角的坐标,而 translationX 和 translationY 是 view 左上角相对于父容器的偏移量,默认值都是 0。x = left + translationX ;y = top + translationY
  • MotionEvent 主要有 ACTION_UPACTION_DOWNACTION_MOVEACTION_CANCEL 等。
    1. 点击一次,事件序列是 ACTION_DOWN -> ACTION_UP
    2. 点击后滑动一会再离开,事件序列是 ACTION_DOWN -> ACTION_MOVE -> ACTION_MOVE -> … -> ACTION_UP
    3. ACTION_CANCEL 触发时机:子 view 接受到了 ACTION_DOWN 事件,父容器拦截了 ACTION_MOVE、ACTION_UP 之后,子 view 会收到 ACTION_CANCEL
    4. 如果 view 不处理 ACTION_DOWN 则后面的事件都不会再给它
    5. ACTION_DOWN 会让 ViewGroup 重置滑动标记(requestDisallowInterceptTouchEvent)
  • getRawX 和 getRawY 是相对于手机屏幕左上角的 x 和 y 坐标。

绘制流程

ViewRootImpl.performTravserals() 负责对所有 view 递归绘制

1、onMeasure()

  • 测量模式 MeasureSpec 是一个 32 位的 int 值,其中高 2 位是测量的模式,低 30 位是测量的大小 (使用位运算是为了提高效率和节省空间)
  • EXACTLY :精确值模式,属性设置为精确数值或者 match_parent 时
  • AT_MOST :最大值模式,属性设置为 wrap_content 时
  • UNSPECIFIED :不指定大小测量模式,父容器不对大小限制,通常情况下用于系统内部多次 Measure 或在绘制自定义 View 时才会用到,表示一种测量的状态
  • View 类默认的 onMeasure() 方法只支持 EXACTLY 模式,所以如果在自定义 View 的时候不重写 onMeasure 方法的话,就只能使用 EXACTLY 模式。自定义 View 可以响应你指定的具体的宽高值或者是 match_parent 属性, 但是如果要让自定义 View 支持 wrap_content 属性的话,那么就必须要重写 onMeasure 方法来指定 wrap_content 时 view 的大小。
  • 对于 DecorView,它的 MeasureSpec 由 window 尺寸和自己的 LayoutParams 决定
  • 对于普通 View,它的 MeasureSpec 由父容器的 MeasureSpec 和自己的 LayoutParams 决定

2、onLayout()

  • 它的作用是 ViewGroup 确定子元素的位置,onLayout() --> layout()
  • 通过 serFrame() 确定 view 四个顶点的位置(mLeft、mRight、mTop、mBittom)

getWidth 和 getMeasureWidth 区别

  • 在 view 默认实现中二者值相等
  • getWidth/getHeight 形成在 layout 过程中,也就是二者的赋值时机不同
  • 改写 layout 方法可以让 二者不同

LinearLayout 和 RelativeLayout 有何不同?

  • RelativeLayout 需要对其子 View 进行两次 measure 过程。而 LinearLayout 则只需一次 measure 过程,所以显然会快于 RelativeLayout,但是如果 LinearLayout 中有 weight 属性,则也需要进行两次 measure
  • RelativeLayout 的子 View 如果高度和 RelativeLayout 不同,会引发效率问题,可以使用 padding 代替 margin 以优化此问题
  • 参考

3、onDraw()

  1. 绘制背景
  2. 绘制内容
  3. 绘制子元素
  4. 绘制滚动条

自定义 view

  • 继承 view 重写 onDraw 方法需要自己支持 wrap_content,并且 padding 也要自己处理(onMeasure、onLayout)。继承特定的 View 例如 TextView 不需要考虑。
  • 尽量不要在 View 中使用 Handler,因为 view 内部本身已经提供了 post 系列的方法,完全可以替代 Handler 的作用。
  • view 中如果有线程或者动画,需要在 onDetachedFromWindow 方法中及时停止。
  • 处理好 view 的滑动冲突情况。

事件分发

这是段伪代码,能说明整个流程

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}
  • public boolean dispatchTouchEvent(MotionEvent ev)
    属于 View 的方法, 父容器、子 view 都有 ,用来进行事件的分发。如果事件能够传递给当前 view,那么此方法一定会被调用,返回结果受当前 view 的 onTouchEvent 和下级 view 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
  • public boolean onInterceptTouchEvent(MotionEvent event)
    在 dispatchTouchEvent 方法内部调用,用来判断是否拦截某个事件,**只有 ViewGroup 才有。**如果当前 view 拦截了某个事件,那么在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。
    若返回值为 True 事件会传递到自己的 onTouchEvent();
    若返回值为 False 传递到子 view 的 dispatchTouchEvent()。
  • public boolean onTouchEvent(MotionEvent event)
    在 dispatchTouchEvent 方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 view 无法再次接收到事件。
    若返回值为 True,事件由自己处理,后续事件序列让其处理;
    若返回值为 False,自己不消耗事件, 向上返回让父容器的 onTouchEvent 接受处理,一直到 Activity 的 onTouchEvent
  • 优先级:OnTouch > onTouchEvent > OnClick > OnLongClick
  • 某个 view 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,那么同一事件序列的其他事件都不会再交给它来处理,并且**后续事件将重新交给它的父容器去处理(父容器的 onTouchEvent 方法);**如果它消耗 ACTION_DOWN 事件,但是不消耗其他类型事件,那么这个点击事件会消失,父容器的 onTouchEvent 方法不会被调用,当前 view 依然可以收到后续的事件,但是这些事件最后都会传递给 Activity 处理。
  • ViewGroup 默认不拦截任何事件,因为它的 onInterceptTouchEvent 方法默认返回 false
  • View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 view 是 disable 状态,只要它的 clickable 或者 longClickable 有一个是 true,那么它的 onTouchEvent 就会返回 true。
  • ViewGroup 的 dispatchTouchEvent 方法中有一个标志位 FLAG_DISALLOW_INTERCEPT ,这个标志位就是通过子 view 调用 requestDisallowInterceptTouchEvent 方法来设置的,一旦设置为 true,那么 ViewGroup 不会拦截该事件。

由此可见,整个事件分发是由 Acitivity 开始的向下递归过程

滑动

  1. 通过 view 本身提供的 scrollTo 和 scrollBy 方法:操作简单,适合对 view 内容的滑动;
  2. 通过动画给 view 施加平移效果来实现滑动:操作简单,适用于没有交互的 view 和实现复杂的动画效果;
  3. 通过改变 view 的 LayoutParams 使得 view 重新布局从而实现滑动:操作稍微复杂,适用于有交互的 view。

scrollTo 和 scrollBy 方法只能改变 view 内容的位置而不能改变 view 在布局中的位置。 scrollBy 是基于当前位置的相对滑动,而 scrollTo 是基于所传参数的绝对滑动。通过 View 的 getScrollX 和 getScrollY 方法可以得到滑动的距离。

Scroller

Scroller 的工作原理:Scroller 本身并不能实现 view 的滑动,它需要配合 view 的 computeScroll 方法才能完成弹性滑动的效果,它不断地让 view 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 view 的当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 view 的滑动。就这样,view 的每一次重绘都会导致 view 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。

使用:

  1. 初始化 Scroller 对象,一般在 view 初始化的时候同时初始化 scroller;
  2. 重写 view 的 computeScroll 方法,computeScroll 方法是不会自动调用的,只能通过 invalidate->draw->computeScroll 来间接调用,实现循环获取 scrollX 和 scrollY 的目的,当移动过程结束之后,Scroller.computeScrollOffset 方法会返回 false,从而中断循环;
  3. 调用 Scroller.startScroll 方法,将起始位置、偏移量以及移动时间(可选) 作为参数传递给 startScroll 方法。
private void ininView(Context context) {
    setBackgroundColor(Color.BLUE);
    // 初始化 Scroller
    mScroller = new Scroller(context);
}

@Override
public void computeScroll() {
    super.computeScroll();
    // 判断 Scroller 是否执行完毕
    if (mScroller.computeScrollOffset()) {
        ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
        // 通过重绘来不断调用 computeScroll
        invalidate();//很重要
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastX = (int) event.getX();
            lastY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            int offsetX = x - lastX;
            int offsetY = y - lastY;
            ((View) getParent()).scrollBy(-offsetX, -offsetY);
            break;
        case MotionEvent.ACTION_UP:
            // 手指离开时,执行滑动过程
            View viewGroup = ((View) getParent());
            mScroller.startScroll( viewGroup.getScrollX(), viewGroup.getScrollY(),
                    -viewGroup.getScrollX(), -viewGroup.getScrollY());
            invalidate();//很重要
            break;
    }
    return true;
}

滑动冲突

分两类:

  • 外部滑动方向与内部滑动方向一致
  • 外部滑动方向与内部滑动方向不一致

1、外部拦截法

重写父容器的 onInterceptTouchEvent 方法,根据适当条件来决定 return true/false ,其他均不需要做修改。

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: {
        int deltaX = x - mLastXIntercept;
        int deltaY = y - mLastYIntercept;
        if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
            intercepted = true;
        } else {
            intercepted = false;
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
    }
    default:
        break;
    }

    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}

2、内部拦截法

父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。 需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作

public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
	    case MotionEvent.ACTION_DOWN: {
	        getParent().requestDisallowInterceptTouchEvent(true);
	        break;
	    }
	    case MotionEvent.ACTION_MOVE: {
	        int deltaX = x - mLastX;
	        int deltaY = y - mLastY;
	        if (当前 view 需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
	            getParent().requestDisallowInterceptTouchEvent(false);
	        }
	        break;
	    }
	    case MotionEvent.ACTION_UP: {
	        break;
	    }
	    default:
	        break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

沧笙踏歌

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

内心激荡

文章 0 评论 0

JSmiles

文章 0 评论 0

左秋

文章 0 评论 0

迪街小绵羊

文章 0 评论 0

瞳孔里扚悲伤

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文