博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 自定义View基础(一)
阅读量:5978 次
发布时间:2019-06-20

本文共 11891 字,大约阅读时间需要 39 分钟。

View的分类

类别 解释 特点
单一视图 即一个View,如TextView 不包含子View
视图组 即多个View组成的ViewGroup,如LinearLayout 包含子View

创建完全自定义的组件

创建自定义的组件主要围绕着以下五个方面:

  • 绘图(Drawing): 控制视图的渲染,通常通过覆写onDraw方法来实现
  • 交互(Interaction): 控制用户和视图的交互方式,比如OnTouchEvent,gestures
  • 尺寸(Measurement): 控制视图内容的维度,通过覆写onMeasure方法
  • 属性(Attributes): 在XML中定义视图的属性,使用TypedArray来获取属性值
  • 持久化(Persistence): 配置发生改变时保存和恢复状态,通过onSaveInstanceState和onRestoreInstanceState

由于Android的2D渲染现在可以比较好的支持硬件加速了,但是在自定义控件进行绘制是还是有很多api不兼容的,所以在自定义控件的时候,在你不能100%确认你使用的api支持硬件加速的话,最好把硬件加速关闭了,否则有可能出现一些莫名其妙的问题:

  1. 硬件加速关闭方法

在清单文件的application节点下进行关闭或者打开,这种方式是作用于整个应用的:

android:hardwareAccelerated="false"复制代码
  1. 在activity注册时进行关闭或者打开,这种方式只作用于该activity:
复制代码

3、在指定View初始化时关闭或者打开,这种方式只作用于该View控件:

//如果是自定义的view,可在构造方法中调用该方法,即可开启或者关闭硬件加速setLayerType(View.LAYER_TYPE_SOFTWARE, null);复制代码

View类简介

  • View类是Android中各种组件的基类,如View是ViewGroup基类
  • View表现为显示在屏幕上的各种视图

View的构造函数

共有4个,具体如下:

// 如果View是在Java代码里面new的,则调用第一个构造函数    public CustomView(Context context) {        super(context);    }    // 如果View是在.xml里声明的,则调用第二个构造函数    // 自定义属性是从AttributeSet参数传进来的    public CustomView(Context context, AttributeSet attrs) {        super(context, attrs);    }    // 不会自动调用    // 一般是在第二个构造函数里主动调用    // 如View有style属性时   public CustomView(Context context,  AttributeSet attrs,                      int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    //API21之后才使用    // 不会自动调用    // 一般是在第二个构造函数里主动调用    // 如View有style属性时    @TargetApi(21)    public CustomView(Context context,  AttributeSet attrs,                      int defStyleAttr, int defStyleRes) {        super(context, attrs, defStyleAttr, defStyleRes);    }复制代码

添加视图到布局中

xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
复制代码

自定义属性

视图可以通过XML来配置属性和样式,你需要想清楚要添加那些自定义的属性,比如我们想让用户可以选择形状的颜色、是否显示形状的名称,比如我们想让视图可以像下面一样配置:

复制代码

为了能够定义shapeColor和displayShapeName,我们需要在res/values/中新建一个文件名为custom_view_attrs.xml的文件(文件名随意),在这个文件中包含<resources></resources>标签,添加<declare-styleable name="ShapeSelectorView"></declare-styleable>标签,标签的name属性通常是自定义的类名,在declare-styleable中添加attr元素,attr元素是key (“name=”) -- value (“format=”)的形式:

复制代码

对于每个你想自定义的属性你需要定义attr节点,每个节点有name和format属性,format属性是我们期望的值的类型,比如color,dimension,boolean,integer,float等。一旦定义好了属性,你可以像使用自带属性一样使用他们,唯一的区别在于你的自定义属性属于一个不同的命名空间,你可以在根视图的layout里面定义命名空间,一般情况下你只需要这样指定:http://schemas.android.com/apk/res/<package_name>,但是你可以使用http://schemas.android.com/apk/res-auto自动解析命名空间。

应用自定义属性

  • 方式一
public class CustomView extends View {    private int shapeColor;    private boolean displayShapeName;    public CustomView(Context context) {        super(context);        initCustomView(null);    }    public CustomView(Context context, AttributeSet attrs) {        super(context, attrs);        initCustomView(attrs);    }    public CustomView(Context context, AttributeSet attrs,                      int defStyleAttr) {        super(context, attrs, defStyleAttr);        initCustomView(attrs);    }    @TargetApi(21)    public CustomView(Context context, AttributeSet attrs,                      int defStyleAttr, int defStyleRes) {        super(context, attrs, defStyleAttr, defStyleRes);        initCustomView(attrs);    }    private void initCustomView(AttributeSet attrs) {        if (attrs == null) {            return;        }        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);        try {            shapeColor = a.getColor(R.styleable.CustomView_shapeColor, Color.WHITE);            displayShapeName = a.getBoolean(R.styleable.CustomView_displayShapeName, false);        } finally {            a.recycle();        }    }}复制代码
  • 方式二
public class CustomView extends View {    private int shapeColor;    private boolean displayShapeName;    public CustomView(Context context) {        this(context, null);    }    public CustomView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public CustomView(Context context, AttributeSet attrs,                      int defStyleAttr) {        this(context, attrs, defStyleAttr, 0);    }    @TargetApi(21)    public CustomView(Context context, AttributeSet attrs,                      int defStyleAttr, int defStyleRes) {        super(context, attrs, defStyleAttr, defStyleRes);        if (attrs == null) {            return;        }        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);        try {            shapeColor = a.getColor(R.styleable.CustomView_shapeColor, Color.WHITE);            displayShapeName = a.getBoolean(R.styleable.CustomView_displayShapeName, false);        } finally {            a.recycle();        }    }}复制代码

建议使用方式一,比如你自定义的View继承自ListView或者TextView的时候,ListView或者TextView内部的构造函数会有一个默认的defStyle, 第二种方法调用时defStyle会传入0,这将覆盖基类中默认的defStyle,进而导致一系列问题。

接下来添加一些getter和setter方法

public class ShapeSelectorView extends View {  // ...  public boolean isDisplayingShapeName() {    return displayShapeName;  }  public void setDisplayingShapeName(boolean state) {    this.displayShapeName = state;    // 当视图属性发生改变的时候可能需要重新绘图    invalidate();    requestLayout();  }  public int getShapeColor() {    return shapeColor;  }  public void setShapeColor(int color) {    this.shapeColor = color;    invalidate();    requestLayout();  }}复制代码

当视图属性发生改变的时候可能需要重新绘图,你需要调用`invalidate()`和`requestLayout()`来刷新显示。

Android 绘制顺序

draw()

draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方法里。背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。

// View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦):public void draw(Canvas canvas) {    ...    drawBackground(Canvas); // 绘制背景(不能重写)    onDraw(Canvas); // 绘制主体    dispatchDraw(Canvas); // 绘制子 View    onDrawForeground(Canvas); // 绘制滑动相关和前景    ...}复制代码

从上面的代码可以看出,onDraw() dispatchDraw() onDrawForeground() 这三个方法在 draw() 中被依次调用,因此它们的遮盖关系就是——dispatchDraw() 绘制的内容盖住 onDraw() 绘制的内容;onDrawForeground() 绘制的内容盖住 dispatchDraw() 绘制的内容。而在它们的外部,则是由 draw() 这个方法作为总的调度。所以,你也可以重写 draw() 方法来做自定义的绘制。

想在滑动边缘渐变、滑动条和前景之间插入绘制代码?虽然这三部分是依次绘制的,但它们被一起写进了 onDrawForeground()方法里,所以你要么把绘制内容插在它们之前,要么把绘制内容插在它们之后。而想往它们之间插入绘制,是做不到的。

写在 super.draw() 的下面

由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的下面,那么这段代码会在其他所有绘制完成之后再执行,也就是说,它的绘制内容会盖住其他的所有绘制内容。

它的效果和重写 onDrawForeground(),并把绘制代码写在 super.onDrawForeground() 的下面效果是一样的:都会盖住其他的所有内容。

当然了,虽说它们效果一样,但如果你既重写 draw() 又重写 onDrawForeground(),那么 draw() 里的内容还是会盖住 onDrawForeground() 里的内容的。所以严格来讲,它们的效果还是有一点点不一样的。

写在 super.draw() 的上面

由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的上面,那么这段代码会在其他所有绘制之前被执行,所以这部分绘制内容会被其他所有的内容盖住,包括背景。

例如:

EditText重写它的 draw() 方法,然后在 super.draw() 的上方插入代码,以此来在所有内容的底部涂上一片绿色:

public AppEditText extends EditText {    ...    public void draw(Canvas canvas) {        canvas.drawColor(Color.parseColor("#F0FF0000")); // 涂上红色        super.draw(canvas);    }}复制代码

注意:出于效率的考虑,ViewGroup默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程

Android坐标系

其中棕色部分为手机屏幕

View坐标系

View的坐标系统是相对于父控件而言的

  1. 原始位置(不受偏移量影响,单位是像素px)
/* 获取子View左上角距父View顶部的距离 * 即左上角纵坐标 */getTop();       /* 获取子View左上角距父View左侧的距离 * 即左上角横坐标 */getLeft();   /* 获取子View右下角距父View顶部的距离 * 即右下角纵坐标 */getBottom();    /* 获取子View右下角距父View左侧的距离 * 即右下角横坐标 */getRight();     复制代码
  1. 宽高和坐标的关系
width = right - left;height = bottom - top;复制代码
  1. Android 新增的参数

    1. x,y:View的左上角坐标
    2. translationX,translationY:相对于父容器的偏移量(有get/set方法)。

    注意:View在平移过程中,原始位置不会改变。

    // 换算关系x = left + translationXy = top + translationY复制代码
    1. 从API21开始增加了z(垂直屏幕方向)和elevation(浮起来的高度,3D)
  2. dp与px(像素)相互转换代码

// dp转为pxpublic static int dp2px(Context context, float dpValue) {    final float scale = context.getResources().getDisplayMetrics().density;    return (int) (dpValue * scale + 0.5f);}// px转为dppublic static int px2dp(Context context, float pxValue) {    final float scale = context.getResources().getDisplayMetrics().density;    return (int) (pxValue / scale + 0.5f);}复制代码

MotionEvent

  • 手指触摸屏幕后产生的事件,典型事件如下:
ACTION_DOWN–手指刚触摸屏幕ACTION_MOVE–手指在屏幕上移动ACTION_UP–手指从屏幕上分开的一瞬间复制代码
  • MotionEvent获取点击事件发生的坐标
getX (相对于当前View左上角的坐标)getYgetRawX(相对于屏幕左上角的坐标)getRawY复制代码
  • TouchSlop滑动最小距离

    • 滑动小于这个常量,系统将不会认为这是滑动(常量为8dp,使用时系统会自动转为px)
    • 获取方式
    ViewConfiguration.get(getContext()).getScaledTouchSlop();复制代码
  • 示例

float x = 0, y = 0;  @Override  public boolean onTouchEvent(MotionEvent event) {// 获取TouchSlop(滑动最小距离)      float slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();      switch (event.getAction()) {          case MotionEvent.ACTION_DOWN:              Log.e(TAG, "onTouchEvent: " + "按下");              Log.e(TAG, "getX: " + event.getX());              Log.e(TAG, "getY: " + event.getY());              Log.e(TAG, "getRawX: " + event.getRawX());              Log.e(TAG, "getRawY: " + event.getRawY());              x = event.getX();              y = event.getY();              break;          case MotionEvent.ACTION_MOVE:              Log.e(TAG, "onTouchEvent: " + "移动");              break;          case MotionEvent.ACTION_UP:              Log.e(TAG, "onTouchEvent: " + "松开" + x);              if (event.getX() - x > slop) {                  Log.e(TAG, "onTouchEvent: " + "往右滑动" + event.getX());              } else if (x - event.getX() > slop) {                  Log.e(TAG, "onTouchEvent: " + "往左滑动" + event.getX());              } else {                  Log.e(TAG, "onTouchEvent: " + "无效滑动" + event.getX());              }              x = 0;              y = 0;              break;      }      // 返回true,拦截这个事件      // 返回false,不拦截      return true;  }复制代码

GestureDetector

  • 辅助检测用户的单击、滑动、长按、双击等行为
  • 使用
    • 创建一个GestureDetector对象并实现OnGestureListener接口,根据需要实现OnDoubleTapListener接口
    // 解决长按屏幕后无法拖动的现象,但是这样会无法识别长按事件mGestureDetector.setIsLongpressEnable(false);复制代码
  • 接管目标View的onTouchEvent方法
    return mGestureDetector.onTouchEvent(event);复制代码
  • 示例
private GestureDetector mGestureDetector;... ...private void init(Context context){       this.mContext = context;       mGestureDetector = new GestureDetector(mContext,onGestureListener);       mGestureDetector.setOnDoubleTapListener(onDoubleTapListener);       //解决长按屏幕无法拖动,但是会造成无法识别长按事件	//mGestureDetector.setIsLongpressEnabled(false);   }	   @Override   public boolean onTouchEvent(MotionEvent event) {       // 接管onTouchEvent       return mGestureDetector.onTouchEvent(event);   }	   GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {       @Override       public boolean onDown(MotionEvent e) {           Log.i(TAG, "onDown: 按下");           return true;       }	       @Override       public void onShowPress(MotionEvent e) {           Log.i(TAG, "onShowPress: 刚碰上还没松开");       }	       @Override       public boolean onSingleTapUp(MotionEvent e) {           Log.i(TAG, "onSingleTapUp: 轻轻一碰后马上松开");           return true;       }	       @Override       public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {           Log.i(TAG, "onScroll: 按下后拖动");           return true;       }	       @Override       public void onLongPress(MotionEvent e) {           Log.i(TAG, "onLongPress: 长按屏幕");       }	       @Override       public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {           Log.i(TAG, "onFling: 滑动后松开");           return true;       }   };	   GestureDetector.OnDoubleTapListener onDoubleTapListener = new GestureDetector.OnDoubleTapListener() {       @Override       public boolean onSingleTapConfirmed(MotionEvent e) {           Log.i(TAG, "onSingleTapConfirmed: 严格的单击");           return true;       }	       @Override       public boolean onDoubleTap(MotionEvent e) {           Log.i(TAG, "onDoubleTap: 双击");           return true;       }	       @Override       public boolean onDoubleTapEvent(MotionEvent e) {           Log.i(TAG, "onDoubleTapEvent: 表示发生双击行为");           return true;       }   };复制代码

目录结构

转载地址:http://hrpox.baihongyu.com/

你可能感兴趣的文章
5月23日任务 LAMP架构介绍、MySQL、MariaDB介绍、 MySQL安装
查看>>
FCC有意支持Sprint与T-Mobile合并?
查看>>
XMLHttpRequest
查看>>
Linux集群架构(下)——DR模式、keepalived+LVS
查看>>
使用Xshell连接Linux服务器
查看>>
hadoop学习笔记2
查看>>
用钉钉接收zabbix告警
查看>>
MySQL基础
查看>>
Oracle伪列ROWID和ROWNUM
查看>>
网关冗余--王贝的学习笔记
查看>>
《统计学习方法》读书笔记(1)---学习的要素
查看>>
Springboot2.1.3 + redis 实现 cache序列化乱码问题
查看>>
struct 类型指针技巧
查看>>
POJ 1321 棋盘问题 题解
查看>>
js实现购物车数量的增加与减少,js实现购物车数量的自增与自减
查看>>
gitlab部署步骤+汉化
查看>>
linux清理缓存的命令
查看>>
jquery文本折叠
查看>>
springmvc请求参数获取(自动绑定)的几种方法
查看>>
对导航条的改造
查看>>