Android_View详解

来源:互联网 发布:淘宝女童秋季外套新款 编辑:程序博客网 时间:2024/06/07 04:42

Android_View详解


本文由 Luzhuo 编写,转发请保留该信息.
原文: http://blog.csdn.net/Rozol/article/details/72848723


View的绘制
View的事件分发机制
View的时间冲突处理

Activity控件架构

  • 可以使用SDK下Tools目录里的 hierarchyviewer.bat 进行布局分析
  • 当我们在Activity中执行setContentView(R.layout.activity_main)时,是给ContentView设置布局
  • 每个控件都会在界面上占一块矩形区域
    -控件分为:
    • ViewGroup容器控件:
      • 可以包含多个View和ViewGroup
      • ViewGroup以树的结构管理View和ViewGroup, 主要负责下层控件的测量,绘制,事件传递.
    • View控件

View的绘制

  • 关键词: measure [ˈmɛʒɚ] / layout [ˈleˌaʊt] / draw [drɔ]
  • 绘制流程: measure -> layout -> draw

measure

  • 测量View的宽高
  • 代码的重写

    public class MyView extends View {    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }}
    • 我们看看 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 做了什么?

      public class View implements Drawable.Callback, KeyEvent.Callback,        AccessibilityEventSource {    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        // ↓↓↓        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    }    // ↓↓↓    public static int getDefaultSize(int size, int measureSpec) {        int result = size;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        switch (specMode) {        case MeasureSpec.UNSPECIFIED:            result = size;            break;        case MeasureSpec.AT_MOST:        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result;    }}
      • 看来源码中是用 MeasureSpec 这个类来计算大小的, 我们先来了解下这个类
      • MeasureSpec 这个类是用来测量View的

        • 测量模式:
          • UNSPECIFIED: 不指定测量模式, 你想多大就多大
          • EXACTLY: 精确测量模式, layout_width=”100dp”/”match-parent”都是使用该模式
          • AT_MOST: 最大值模式, layout_width=”wrap_content”时使用该模式
        • 我们来测试下三种模式的区别,在测试之前先修改下 public static int getDefaultSize(int size, int measureSpec) 方法的代码

          • 我们在 case MeasureSpec.AT_MOST: 下添加两行代码,让他在 layout_width=”wrap_content” 时使用默认值和测量值的最小值, 其他代码不变

            case MeasureSpec.AT_MOST:    result = Math.min(size, specSize);    break;
          • 效果:
      • Measure 是对ViewGroup树进行自上而下遍历而确定View最大可用大小的
      • 最后需要调用 setMeasuredDimension(width, height); 设置View的宽高值

layout

  • 确定View在父容器的位置
  • 代码的重写

    @Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {    super.onLayout(changed, left, top, right, bottom);}
  • Layout的位置是对ViewGroup树进行自上而下遍历而确定的

  • 参数说明:
    • changed: View大小是否发生改变
    • left, top, right, bottom: 左上右下的坐标值
  • 实际开发中,ViewGroup会需要重写onLayout来确定子View的位置,View没啥位置可确定的

draw

  • 绘制View
  • 代码的重写

    @Overrideprotected void onDraw(Canvas canvas) { }
  • 仅需调用canvas进行绘制即可

  • Canvas就像画板,Paint就像画笔,使用Paint在Canvas上作画,作画的水平决定这个View控件是否优美.

案例代码

package me.luzhuo.viewdemo;import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Canvas;import android.graphics.Paint;import android.util.AttributeSet;import android.util.Printer;import android.view.MotionEvent;import android.view.View;/** * ================================================= * <p> * Author: Luzhuo * <p> * Version: 1.0 * <p> * Creation Date: 2017/6/1 15:55 * <p> * Description: View的绘制机制 * <p> * Revision History: * <p> * Copyright: Copyright 2017 Luzhuo. All rights reserved. * <p> * ================================================= **/public class ViewDemo extends View {    private Paint paint;    private Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); // 测试图片    public ViewDemo(Context context) {        super(context);        init();    }    public ViewDemo(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public ViewDemo(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        paint = new Paint();    }    /**     * 1. 测量     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(bitmap.getWidth() * 3, widthMeasureSpec), getDefaultSize(bitmap.getHeight() * 3, heightMeasureSpec));    }    public static int getDefaultSize(int size, int measureSpec) {        int result = size;        int specMode = View.MeasureSpec.getMode(measureSpec);        int specSize = View.MeasureSpec.getSize(measureSpec);        switch (specMode) {            case View.MeasureSpec.UNSPECIFIED:                result = size;                break;            case View.MeasureSpec.AT_MOST:                result = Math.min(size, specSize);                break;            case View.MeasureSpec.EXACTLY:                result = specSize;                break;        }        return result;    }    @Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        super.onLayout(changed, left, top, right, bottom);    }    @Override    protected void onDraw(Canvas canvas) {        canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2.0f, y - bitmap.getHeight() / 2.0f, paint);    }    private float x, y;    @Override    public boolean onTouchEvent(MotionEvent event) {        x = event.getX(); // 控件x坐标        y = event.getY();        switch (event.getAction()){            case MotionEvent.ACTION_DOWN:                break;            case MotionEvent.ACTION_MOVE:                break;            case MotionEvent.ACTION_UP:                break;        }        invalidate();        return true;    }}

其他重要方法补充

  • 回调方法:
    • protected void onFinishInflate() { } // XML控件加载完成后回调
    • protected void onSizeChanged(int w, int h, int oldw, int oldh) { } // 控件的大小改变时回调
  • 更新方法:
    • invalidate(); // 重绘(会调用onMeasure()), 如果onMeasure()测量的大小没有发生变化,就不会调用onLayout(), 但是会调用onDraw()
    • requestLayout(); // 重新测量View的大小, 会调用onMeasure()和onLayout(),但是不会调用onDraw()

View的事件分发机制

  • Android上的View可能是重叠在一起的,当我们在手机屏幕上按下时,哪个View该响应呢?事件分发机制就是为解决这个问题的.
  • 触摸事件: 就是触摸屏幕的事件; 触摸事件分为:按下 / 滑动 / 抬起, Android将触摸事件封装为MotionEvent类

    • MotionEvent的几个重要方法:

      • ev.getX(); // 相对于父容器的x坐标
      • ev.getRawX(); // 屏幕x坐标
      • MotionEvent.ACTION_DOWN; // 按下
      • MotionEvent.ACTION_MOVE; // 滑动
      • MotionEvent.ACTION_UP; // 抬起
    • 事件的传递流程

    • 当我们点击最上面的View时, 完成的事件传递是这样的:
      • Activity -> PhoneWindow -> DecorView -> ContentView -> ViewGroup -> ViewGroup2 -> View -> ViewGroup2 -> ViewGroup -> ContentView -> DecorView -> PhoneWindow -> Activity
      • 可见如果事件在传递的过程中都没有被消费, 那么事件将被回传
    • 以下我们将简化传递流程, 并演示 dispatchTouchEvent(事件分发) / onInterceptTouchEvent(事件拦截) / onTouchEvent(触摸事件)
      • 默认
      • ViewGroup2 的 dispatchTouchEvent 返回 true
        • 这里写图片描述
      • ViewGroup2 的 onInterceptTouchEvent 返回 true
      • ViewGroup2 的 onTouchEvent 返回 true
    • 总结
      • dispatchTouchEvent: 分发事件(return true:拦截(事件丢弃), false,不拦截(默认)) (注:返回true,事件不会交由onTouchEvent处理)
      • onInterceptTouchEvent: 拦截事件 (View没有该回调接口) (return: true:拦截事件,交由onTouchEvent处理, false:传递给子View,由子View的dispatchTouchEvent接收(默认))
      • onTouchEvent: 触摸事件(return true:消费了, false:向上级传递(默认))

View的事件冲突处理

  • 事件冲突实际开发中主要为滑动冲突:

    • 解决方案分为:

      • 第一种: 写一个新的ViewGroup继承父ViewGroup, 并重写onInterceptTouchEvent()
      • 第二种: 子View(或ViewGroup)使用getParent().requestDisallowInterceptTouchEvent(true);请求父类不要拦截Touch事件 (父ViewGroup将不会执行onInterceptTouchEvent()回调)
    • 第一种讲解: 较简单,没什么好讲的,贴个代码做参考吧 (这是限制ViewPager是否可左右滑动的案例代码)

      public class CustomViewPager extends ViewPager {    private boolean setTouchModel = false;    public CustomViewPager(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        if(setTouchModel){            return super.onInterceptTouchEvent(ev);        }else{            return false;        }    }    @Override    public boolean onTouchEvent(MotionEvent ev) {        if(setTouchModel){            return super.onTouchEvent(ev);        }else{            return false;        }    }}
    • 第二种讲解:

      • 先看容器ViewGroup2的代码, 拦截滑动事件,并进行处理

        @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    Log.e("ViewGroupEvent2", "onInterceptTouchEvent");    switch (ev.getAction()) {        case MotionEvent.ACTION_MOVE:            return true;    }    return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {    Log.e("ViewGroupEvent2", "onTouchEvent");    return true;}
      • 然后触摸view执行的结果, 可见滑动事件都被ViewGroup2消费掉了,view都没有拿到

      • 然后我们在子view中加入getParent().requestDisallowInterceptTouchEvent(true)

        @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {    Log.e("ViewEvent", "dispatchTouchEvent");    switch (ev.getAction()) {        case MotionEvent.ACTION_DOWN:            System.out.println("requestDisallowInterceptTouchEvent(true)");            getParent().requestDisallowInterceptTouchEvent(true);            break;        case MotionEvent.ACTION_UP:        case MotionEvent.ACTION_CANCEL:            System.out.println("requestDisallowInterceptTouchEvent(false)");            getParent().requestDisallowInterceptTouchEvent(false);            break;    }    return super.dispatchTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {    Log.e("ViewEvent", "onTouchEvent");    return true;}
      • 执行结果如下,可见view已经夺得了滑动事件的控制权

ViewGroup

measure

  • ViewGroup是管理子View的, MeasureSpec.AT_MOST 模式时, ViewGroup会先子View进行遍历, 获取所有子View的大小, 然后决定自己的大小
  • 当子View测量完成后可通过调用 view.getLayoutParams() 获取宽高信息

layout

  • 可通过 view.layout(l, t, r, b); 设置子View的位置

draw

  • 容器没啥可绘制的
原创粉丝点击