Android之自定义View

一.概述

通过本篇文章的学习,你将学会:
1.自定义View的流程
2.自定义View分类

二.自定义View的流程

自定义View主要是用来实现Android系统自带View无法实现的控件,需要我们自己自定义得到我们需要的效果。一般情况下,我们自定义View的步骤总体上是:

  • 自定义属性
  • 自定义构造方法
  • 重写onMeasure方法测量宽高
  • 重写onDraw方法进行绘制
  • 重写onLayout方法负责布局的调整(继承ViewGroup用到)
  • 重写其他方法诸如onTouchEvent()检测用户手指操作
    自定义属性
    我们通常将自定义属性定义在/values/attr.xml文件中(attr.xml文件需要自己创建)。然后创建一个属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--name为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的View一样的名称-->
    <declare-styleable name="CustomeView">
        <!--声明我们的属性,名称为default_size,取值类型为尺寸类型(dp,px等)-->
        <attr name="default_size" format="dimension" />
    </declare-styleable>
</resources>

其中format表示的是数据类型,常见的format有:

  • string:字符串类型
  • integer:整数类型
  • dimension:尺寸,后面必须跟dp、dip、px、sp等单位
  • Boolean:布尔值
  • color:颜色,必须是“#”符号开头
    使用:在XML布局文件中使用自定义的属性时,我们需要先定义一个namespace,Android中默认的namespace是android:
xmlns:android="http://schemas.android.com/apk/res/android"

我们自定义的属性不在这个命名空间下,因此我们需要添加一个命名空间。这个namespace可以随便取,比如:

xmlns:gyq="http://schemas.android.com/apk/res-auto"

但是注意把最后的“res/android”改成了“res-auto”,这样我们就能在XML文件中使用自定义的属性。
自定义构造方法
我们继承View或者已有View之后至少需要重写一个构造函数,我们需要重写含AttributeSet 的构造函数
CustomeView(Context context, AttributeSet attrs)去获取我们的自定义属性

private int default_size;
public CustomeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
        TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.CustomeView);
        //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        default_size=ta.getDimensionPixelSize(R.styleable.CustomeView_default_size,100);
        //最后记得将TypedArray对象回收
        ta.recycle();
    }

注意:在TypedArray使用结束后,需要调用recycle()方法回收它。不管有没有使用自定义属性,都会默认调用两个参数的构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。
重写onMeasure方法测量宽高
onMeasure()方法中主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。widthMeasureSpec和heightMeasureSpec是int值,int占用的32bit中的前面两个bit用于表示测量模式,后面30个bit用于表示测量值。这里的的测量值并不是最终我们的View的尺寸大小,而是父View提供的参考大小。我们需要结合测量模式获取大小,测量模式有:

  • EXACTLY:当宽高值设置为具体值时使用,如100dp、match_parent等,此时取出的size是精确的尺寸
  • AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的尺寸
  • UNSPECIFIED:当没有指定宽高值时使用(很少见)
    也就是说,在EXACTLY模式,宽高就用得到的测量值,在AT_MOST模式,测量值是最大尺寸,可以通过计算childView.getMeasuredHeight()获取实际宽高。
    onMeasure:
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int resultWidth, resultHeight;
        resultWidth = default_size;//默认尺寸
        resultHeight = default_size;//默认尺寸
        //宽
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        Log.i("lvv","width:"+width);//720
        if (widthMode == MeasureSpec.EXACTLY) {
            resultWidth = width;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            resultWidth = width > default_size ? default_size : width;//取小于width的值
        }
        //高
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int heighMode = MeasureSpec.getMode(heightMeasureSpec);
        Log.i("lvv","height:"+height);//1048
        if (heighMode == MeasureSpec.EXACTLY) {
            resultHeight = height;
        }else if (heighMode == MeasureSpec.AT_MOST) {
            resultHeight = height > default_size ? default_size : height;//取小于height的值
        }
        //正方形,宽高取更小值
        if (resultHeight > resultWidth) {
            resultHeight = resultWidth;
        } else {
            resultWidth = resultHeight;
        }
        setMeasuredDimension(resultWidth, resultHeight);
    }

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:gyq="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.tinkertest.tinkertest.view.CustomeView
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        gyq:default_size="100dp"
        android:background="#f00"/>
</LinearLayout>
image.png

如果布局改为:

<com.tinkertest.tinkertest.view.CustomeView
        android:layout_width="200dp"
        android:layout_height="match_parent"
        gyq:default_size="100dp"
        android:background="#f00"/>

image.png

解释:第一种情况如果高用的是wrap_content,那么高的模式就是MeasureSpec.AT_MOST,则高会选择默认的高度100dp,这个时候的layout_width在大于100dp的时候不管是多大,正方形宽高都是100dp。第二中情况高用的是match_parent,那么高模式就是MeasureSpec.EXACTLY,是精确尺寸,使用测量高度,这个时候的layout_width决定了正方形的宽高。
重写onDraw方法进行绘制
onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。在onDraw()方法中使用最多的两个类:Paint和Canvas。
Paint
用于设置如何绘制几何图形、文字和位图的样式和颜色信息,用于辅助Canvas绘制

  • 文本绘制
    setTextAlign(Path.Align a):设置绘制的文本的对齐方式;
    setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;
    setTextSize(float s):设置字号;
    setTextSkewX(float s):设置斜体文字,s是文字倾斜度;
    setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;
    setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;
    setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;
    setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果;
  • 图形绘制
    setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;
    setAlpha(int a):设置绘制的图形的透明度;
    setColor(int color):设置绘制的颜色;
    setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;
    setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;
    setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;
    setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;
    setPathEffect(PathEffect pe):设置绘制的路径的效果;
    setShader(Shader s):设置Shader绘制各种渐变效果;
    setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;
    setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;
    Canvas
    Canvas即画布,可以使用Paint画笔对象绘制什么东西
    drawArc():绘制圆弧;
    drawBitmap():绘制Bitmap图像;
    drawCircle():绘制圆圈;
    drawLine():绘制线条;
    drawOval():绘制椭圆;
    drawPath():绘制Path路径;
    drawPicture():绘制Picture图片;
    drawRect():绘制矩形;
    drawRoundRect():绘制圆角矩形;
    drawText():绘制文本;
    drawVertices():绘制顶点。
    使用如下的onDraw方法绘制一个绿色的圆形或者椭圆
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
        //圆心的横坐标为当前的View的左边起始位置+半径
        int centerX = getLeft() + r;
        //圆心的纵坐标为当前的View的顶部起始位置+半径
        int centerY = getTop() + r;

        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        //绘制圆
        canvas.drawCircle(centerX, centerY, r, paint);
        //绘制椭圆
//        canvas.drawOval(0,0,getMeasuredWidth(),getMeasuredHeight()/2,paint);
    }

效果图:


圆形

椭圆

重写onLayout方法负责布局的调整
onLayout()方法负责布局,是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。在其中常用的方法有:

  • getChildCount():获取子View的数量
  • getChildAt(i):获取第i个子View
  • getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距
  • child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值
  • child.layout(l, t, r, b):设置子View布局的上下左右边的坐标
    使用如下的onlayout对自定义ViewGroup的子View进行布局:
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //记录当前的高度位置
        int curHeight = t;
        //将子View逐个摆放
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            //摆放子View,参数分别是子View矩形区域的左、上、右、下边
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }

child.layout()方法中可以看出是将子View进行垂直布局,实际上达到LinearLayout的效果。
重写其他方法
其他的方法比如onTouchEvent():用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。同时这个也往往结合GestureDetector手势动作探器类,如下面的继承ListView并实现OnTouchListener,OnGestureListener触摸和手势动作接口去实现侧滑条目出现删除按钮。

// 手势动作探测器
      private GestureDetector mGestureDetector;
// 在构造函数创建手势监听器对象
      mGestureDetector = new GestureDetector(getContext(), this);
// 在构造函数监听onTouch事件         
      setOnTouchListener(this);
 // 触摸监听事件
      @Override
      public boolean onTouch(View v, MotionEvent event) {
          if (isDeleteShown) {
              hideDelete();
              return false;
          } else {
              return mGestureDetector.onTouchEvent(event);
          }
      }

      @Override
      public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
              float velocityY) {
          // 如果当前删除按钮没有显示出来,并且x方向滑动的速度大于y方向的滑动速度
          if (!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) {
              mDeleteBtn = LayoutInflater.from(getContext()).inflate(
                      R.layout.delete_btn, null);
  
              mDeleteBtn.setOnClickListener(new OnClickListener() {
  
                  @Override
                  public void onClick(View v) {
                      mItemLayout.removeView(mDeleteBtn);
                      mDeleteBtn = null;
                      isDeleteShown = false;
                      //对外接口去删除当前条目的数据
                      mOnDeleteListener.onDelete(mSelectedItem);
                  }
              });
              //添加删除按钮的布局
              mItemLayout = (ViewGroup) getChildAt(mSelected - getFirstVisiblePosition());
              RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                      LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
              params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
              params.addRule(RelativeLayout.CENTER_VERTICAL);
  
              mItemLayout.addView(mDeleteBtn, params);
              isDeleteShown = true;
          }
          return false;
     }

效果图:


image.png

三.自定义View的分类

看到有点文章上将其分为三种,组合控件、自绘控件和继承控件。不过,通过文章第二部分的讲解,我觉得严格意义上来说就分为自绘控件和继承控件两种。

  • 自绘控件的内容都是自己绘制出来的,在View的onDraw方法中完成绘制。
  • 继承控件就是继承已有的控件,创建新控件,保留继承的父控件的特性,并且还可以引入新特性。
    基本上自定义View就是这些内容,不过,这只是刚刚入门,以后会碰到更多的新花样可能需要我们通过自定义View去实现。

四.总结

以上就是关于自定义View的知识点,如有不足或者错误的地方请指正。不管怎样,代码不只是需要多看,更需要通过自己动手去写去熟悉才能有更深的印象,更好更全面的了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,513评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,312评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,124评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,529评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,937评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,913评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,084评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,816评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,593评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,788评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,267评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,601评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,265评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,158评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,953评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,066评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,852评论 2 277

推荐阅读更多精彩内容