Android 你不知道的自定义View(一)

    说起Android 自定义View,网上的博客、视频很多。鸿洋的博客和视频还是很值得推荐的。本文打算结合Sdk源码,来讲解如何自定义一个View。

    本文结合TextView的源码,看看怎么实现一个简单的自定义View。如果你想下载源码,可以看看这篇文章,Ubuntu完美下载Android源码。 有源码后,可以使用Source Insight这个工具打开。如果没有Android源码,但是有SDK的jar包源码,那么使用IDE工具中就可以查看SDK的源码!

    自定义View的步骤一般有以下4步:

(1). 自定义View的属性;

(2). 在View的构造方法中获取自定义的属性以及属性值;

(3). 重写onMeasure();

(4). 重写onDraw() 。

    接下来,我们就结合TextView的源码来实现一个简单的自定义View。

1. 自定义View的属性。

首先看看Android framework源码attrs.xml中有关TextView的属性的代码中是如何实现的,代码示例:

<declare-styleable name="TextView">  
       ...  
       <!-- Text color. -->  
       <attr name="textColor" />  
       <!-- Color of the text selection highlight. -->  
       <attr name="textColorHighlight" />  
       <!-- Color of the hint text. -->  
       <attr name="textColorHint" />  
       <!-- Base text color, typeface, size, and style. -->  
       <attr name="textAppearance" />  
       <!-- Size of the text. Recommended dimension type for text is "sp" for scaled-pixels (example: 15sp). -->  
       <attr name="textSize" />  
       <!-- Sets the horizontal scaling factor for the text. -->  
       <attr name="textScaleX" format="float" />  
       <!-- Typeface (normal, sans, serif, monospace) for the text. -->  
       <attr name="typeface" />  
       <!-- Style (bold, italic, bolditalic) for the text. -->  
       <attr name="textStyle" />  
       <!-- Text color for links. -->  
       <attr name="textColorLink" />  
       <!-- Makes the cursor visible (the default) or invisible. -->  
       <attr name="cursorVisible" format="boolean" />  
       <!-- Makes the TextView be at most this many lines tall. -->  
       <attr name="maxLines" format="integer" min="0" />  
       <!-- Makes the TextView be at most this many pixels tall. -->  
       <attr name="maxHeight" />  
       <!-- Makes the TextView be exactly this many lines tall. -->  
       <attr name="lines" format="integer" min="0" />  
       <!-- Makes the TextView be exactly this many pixels tall.  
            You could get the same effect by specifying this number in the  
            layout parameters. -->  
       <attr name="height" format="dimension" />  
       <!-- Makes the TextView be at least this many lines tall. -->  
       <attr name="minLines" format="integer" min="0" />  
       <!-- Makes the TextView be at least this many pixels tall. -->  
       <attr name="minHeight" />  
       <!-- Makes the TextView be at most this many ems wide. -->  
       <attr name="maxEms" format="integer" min="0" />  
       <!-- Makes the TextView be at most this many pixels wide. -->  
       <attr name="maxWidth" />  
       <!-- Makes the TextView be exactly this many ems wide. -->  
       <attr name="ems" format="integer" min="0" />  
       <!-- Makes the TextView be exactly this many pixels wide.  
            You could get the same effect by specifying this number in the  
            layout parameters. -->  
       <attr name="width" format="dimension" />  
       <!-- Makes the TextView be at least this many ems wide. -->  
       <attr name="minEms" format="integer" min="0" />  
       <!-- Makes the TextView be at least this many pixels wide. -->  
       <attr name="minWidth" />  
       ...  
   </declare-styleable>  
可以看出,自定义属性,需要用到<declare-styleable>标签,定义属性,使用‘attr’标签,格式类似‘<attr name="textScaleX" format="float" />’,‘name’是属性名,‘format’是属性的类型。

PS:有关<declare-styleable>标签的详细说明,可以看这篇文章, Android View(四)-View相关属性详解
看完源码后,我们可以仿照源码的写法,来编写自定义属性。在res/values文件夹下的atts.xml,创建我们需要的view属性。如果没有atts.xml,请手动创建。具体代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="AduioView">

        <attr name="firstColor" format="color"/>
        <attr name="secondColor" format="color"/>
        <attr name="progress" format="integer"/>
       
    </declare-styleable>
</resources>

以上就是自定义属性,是不是很简单呢!

2. 在View的构造方法中获取自定义的属性以及属性值。

老规矩,还是先看Textview是如何实现的,上代码:

public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
    ...
	
    public TextView(Context context) {
        this(context, null);
    }

    public TextView(Context context,
                    AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.textViewStyle);
    }

    @SuppressWarnings("deprecation")
    public TextView(Context context,
                    AttributeSet attrs,
                    int defStyle) {
        super(context, attrs, defStyle);
     ...            
   TypedArray a = theme.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);    
   ...        
   a = theme.obtainStyledAttributes( attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);  
     int n = a.getIndexCount();
   for (int i = 0; i < n; i++) {
   int attr = a.getIndex(i);
     switch (attr) {
     case com.android.internal.R.styleable.TextView_editable:
        editable = a.getBoolean(attr, editable);
        break;

     case com.android.internal.R.styleable.TextView_inputMethod:
        inputMethod = a.getText(attr);
        break;
            ...
            }
        }
     a.recycle(); 
     ...       
     }
     ...
}
只罗列了重要的代码,但是这些就足够说明问题了。

    回到代码,有三个构造方法,分别是一个参数、两个参数、三个参数,并且一个参数的构造方法调用两个参数的构造方法,两个参数的构造方法调用三个参数的构造方法,三个参数的构造方法调用父类的构造方法。那么我们重点看看三个参数的构造方法。其中,

(1). 通过TypedArray获取自定义的属性集合。有关TypedArray的详细说明,可以看这篇文章, Android View(四)-View相关属性详解

TypedArray a = theme.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
(2). 分别获取自定义属性。循环从属性集合中获取属性值。

(3). 记得最后要释放TypedArray,调用a.recycle()。

PS:

1. 好多文章在讲解自定义View时,获取属性值这一步的实现可能是底下这一种方式,具体代码如下:

        String text = array.getString(R.styleable.BottomWidget_tv_text);
        float  textSize = array.getDimension(R.styleable.BottomWidget_tv_textSize, 0);
        int textColor = array.getColor(R.styleable.BottomWidget_tv_textColor, 0);
        int background = array.getDrawable(R.styleable.BottomWidget_iv_background);
        array.recycle();
首先这种写法并没有错,但是这种写法有一个坑,就是当某一个属性,没有设置值时,它也会给该属性一个默认值,这样的话,就可能会出问题。所以在此建议,在获取自定义View属性值时,使用循环从属性集合中获取属性值,具体代码如下所示:

for (int i = 0; i < n; i++) {
    int attr = a.getIndex(i);
     switch (attr) {
        case com.android.internal.R.styleable.TextView_editable:
            editable = a.getBoolean(attr, editable);
            break;
    ...
}
}
2. 有关构造方法到底是调用自己的方法还是调用父类的。

源码中,我们看到了的现象是,一个参数的构造方法调用两个参数的构造方法,两个参数的构造方法调用三个参数的构造方法,三个参数的构造方法调用父类的构造方法;但是如果我们自定义的View是继承自某一个控件,例如Button,那么建议,构造方法调用的规则是,构造方法调用相应的父类构造方法。因为只有这样,该自定义View才能继承父View的一些样式。

总结:如果我们自定义的View是继承至某一个控件,需要使用到该控件的样式,那么构造方法要调用相应的父类构造方法,代码是‘super(...)’;如果我们是集成自View,那么就可以成‘this(...)’。

下面,我们就根据上面的描述,获取自定义属性值,代码如下,

    private int firstColor;//第一种颜色
    private int secondeColor;//第二种颜色
    private int progress = 1;//当前音量

    private int firstColorDefault = Color.BLUE;//默认颜色
    private int secondColorDefault = Color.RED;//默认颜色
    private int progressDefault = 0;//默认值

    private int splitSize = 5;//间隔高度
    private int mWidth = 100;//每个小块的宽度
    private int mHeight =30;//每个小块的高度
    private final int maxProgress = 10;//最大音量

    private Paint mPaint;//画笔
    private float stockWidth = 5;//描边的宽度
    private int stockColor = Color.BLACK;//描边的颜色


    private float left = 0;
    private float top = 0;

    private float right = 0;
    private float bottom = 0;
    public AduioView(Context context) {
        this(context,null);
    }

    public AduioView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public AduioView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final Resources.Theme theme = context.getTheme();
        TypedArray ta = theme.obtainStyledAttributes(attrs, R.styleable.AduioView, defStyleAttr, 0);
        int n = ta.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = ta.getIndex(i);
            switch (attr) {
                case R.styleable.AduioView_firstColor:
                    firstColor = ta.getColor(attr, firstColorDefault);
                    break;
                case R.styleable.AduioView_secondColor:
                    secondeColor = ta.getColor(attr, secondColorDefault);
                    break;
                case R.styleable.AduioView_progress:
                    progress = ta.getInteger(attr, progressDefault);
                    break;
            }
        }
        ta.recycle();
        mPaint = new Paint();

    }  

获取到自定义属性值后,就可能需要测量以及绘制。那么第三步,我们先绘制,检验一下不测量先绘制的影响

3.  重写onDraw() 方法。
首页,还是看看Textview的onDraw()是如何实现的,上代码:

 @Override
    protected void onDraw(Canvas canvas) {
        restartMarqueeIfNeeded();

        // Draw the background for this view
        super.onDraw(canvas);

        ...
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        final int right = mRight;
        final int left = mLeft;
        final int bottom = mBottom;
        final int top = mTop;
        final boolean isLayoutRtl = isLayoutRtl();
        final int offset = getHorizontalOffsetForDrawables();
        final int leftOffset = isLayoutRtl ? 0 : offset;
        final int rightOffset = isLayoutRtl ? offset : 0 ;

        final Drawables dr = mDrawables;
        if (dr != null) {
            /*
             * Compound, not extended, because the icon is not clipped
             * if the text height is smaller.
             */

            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;

            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
            // Make sure to update invalidateDrawable() when changing this code.
            if (dr.mShowing[Drawables.LEFT] != null) {
                canvas.save();
                canvas.translate(scrollX + mPaddingLeft + leftOffset,
                                 scrollY + compoundPaddingTop +
                                 (vspace - dr.mDrawableHeightLeft) / 2);
                dr.mShowing[Drawables.LEFT].draw(canvas);
                canvas.restore();
            }
			...
        }
		...
	}	
	...
onDraw()方法,无非是在画布(Canvas)上使用画笔(Paint)绘制View。
那接下来,我们就重写onDraw()方法,具体代码如下,

   @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setAntiAlias(true);//设置抗锯齿
        mPaint.setColor(stockColor);//设置描边颜色
        mPaint.setStrokeWidth(stockWidth);//设置描边宽度
        drawOval(canvas);//绘制矩形
    }

    /*
     * 绘制图形
     * */
    private void drawOval(Canvas canvas) {
        left = 0;// 左坐标
        right = 100;// 右坐标
        bottom = mHeight;// 下坐标
        mPaint.setColor(firstColor);//设置画笔的颜色
        //循环计算矩形的坐标点,绘制底部矩形
        for (int i = 0; i < maxProgress; i++) {
            top = i * (mHeight + splitSize);//上坐标(每个矩形的高度+间隔高度)*i
            bottom = i * (mHeight + splitSize) + mHeight;// 下坐标(每个矩形的高度+间隔高度)*i+矩形的高度
            canvas.drawRect(left, top, right, bottom, mPaint);//绘制矩形 (左上角坐标,右下角坐标,画笔)
        }
        mPaint.setColor(secondeColor);//设置画笔的颜色
        //循环计算矩形的坐标点,绘制第二层矩形
        for (int i = 0; i < progress; i++) {
            top = mHeight * (maxProgress - i) + (maxProgress - i - 1) * splitSize - mHeight;//上坐标
            bottom = mHeight * (maxProgress - i) + (maxProgress - i - 1) * splitSize;// 下坐标
            canvas.drawRect(left, top, right, bottom, mPaint);//绘制矩形 (左上角坐标,右下角坐标,画笔)
        }
    }
代码都有注释,不难理解!如果对 画笔(Paint) 和画布 (Canvas) 还不了解,请看这篇文章 Android 绘图(一) Paint   Android 绘图(二) Canvas 。
那么,我们就在布局文件中使用该自定义View,看看它的样子。

打开布局xm文件,首先需要在最外层的ViewGroup中加入命名空间,Android Studio中命名空间的写法是这样,‘ xmlns:aduio="http://schemas.android.com/apk/res-auto"’,其中‘aduio’ 是命名空间。如果是在Eclipse中,命名空间的写法,‘xmlns:aduio="http://schemas.android.com/apk/res/cn.xinxing.customeview"’,其中‘aduio’ 是命名空间,‘cn.xinxing.customeview’是应用的包名。下面是xml的代码,

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:aduio="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
<cn.xinxing.customeview.view.AduioView
    android:layout_width="200dp"
    android:layout_height="500dp"
    aduio:firstColor="#00ff00"
    android:layout_centerInParent="true"
    aduio:progress="2"
    aduio:secondColor="#ff0000"
    />
</RelativeLayout>
如果你使用 Android Studio ,还可以看到设置的颜色,截图如下,所以 推荐使用 Android Studio


现在我们就可以运行该项目了!运行后的效果,截图如下,


到这儿,是不是自定义View就完了呢?非也非也!因为,我们知道onMeasure()方法还未重写!但是没有重写onMeasure()方法,好像也没发现有什么问题!此时,我们修改一下布局文件中引入自定义View的属性,例如修改android:layout_height="wrap_content",并且给该View加入了一个黑色背景。再次运行,看效果,截图如下所示,


是不是很奇怪呢?为何设置‘android:layout_height="wrap_content"’后,高度怎么充满父控件了呢?感觉它的值和‘android:layout_height="match_parent"’是一样的?确实是这样的。通过阅读View的源码可以得出,View中的属性‘android:layout_height=" "’’,当设置为‘wrap_content’或者‘match_parent’,其效果都和‘match_parent’一样的,充满父控件;当设置为一个具体的数值,那么效果基本和设置的值保持一致。所以,在自定义View的时候,我们最好重写onMeasure()方法。

(4). 重写onMeasure()方法。

还是先看看Textview的onMeasure()是如何实现的,上代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        BoringLayout.Metrics boring = UNKNOWN_BORING;
        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;

        int des = -1;
        boolean fromexisting = false;

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
            if (mLayout != null && mEllipsize == null) {
                des = desired(mLayout);
            }
		}
       ...
 }	

通过MeasureSpec这个类,获取到建议的测量模式和测量值,然后根据View自身的特性,最后计算出适合自己的测量值。有关MeasureSpec这个类,可以查看这篇文章, Android View(三)-MeasureSpec详解
接下来,重写o nMeasure()方法,代码如下,

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);//获取宽度的测试模式
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽度的测试值
        int width;
		//如果宽度的测试模式等于EXACTLY,就直接赋值
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = mWidth;//使用我们自己在代码中定义的宽度
		    //如果宽度的测试模式等于AT_MOST,取测量值和计算值的最小值
	        if (widthMode == MeasureSpec.AT_MOST) {  
            width = Math.min(width, widthSize);    
            } 
        }
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);//获取高度的测试模式
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);//获取高度的测试值
        int height;
		//如果高度的测试模式等于EXACTLY,就直接赋值
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
		    //计算出整个View的高度
            height = mHeight * maxProgress + (maxProgress - 1) * splitSize;
		    //如果高度的测试模式等于AT_MOST,取测量值和计算值的最小值
            if (heightMode == MeasureSpec.AT_MOST) {  
            height = Math.min(height, heightSize);    
            }  
        }
        setMeasuredDimension(width, height);//来存储测量的宽,高值
    }

重写onMeasure()方法后,我们再次修改android:layout_height=" "的值,上截图,

       

(android:layout_height="match_parent")                   (android:layout_height="wrap_content")

(android:layout_height="500dp")

效果很明显,分别设置三种不同的值,效果都基本一致!

至此,自定义View就完成了!

总结:

     自定义View的一般步骤就是以上4步,平时按照这几步去实现,就可以了!

PS:例子工程下载路径

推荐文章:Android View(三)-MeasureSpec详解

                   Android View(四)-View相关属性详解

                     

                    Android 绘图(一) Paint

                   Android 绘图(二) Canvas


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值