美文网首页
Android之自定义View

Android之自定义View

作者: 潇洒哥gyq | 来源:发表于2019-04-19 15:11 被阅读0次

    一.概述

    通过本篇文章的学习,你将学会:
    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的知识点,如有不足或者错误的地方请指正。不管怎样,代码不只是需要多看,更需要通过自己动手去写去熟悉才能有更深的印象,更好更全面的了解。

    相关文章

      网友评论

          本文标题:Android之自定义View

          本文链接:https://www.haomeiwen.com/subject/bjwfgqtx.html