自定义控件

6 分钟 阅读

我们先来了解一下Android的控件框架. 在Android中控件大致被分为两类,View和ViewGroup.ViewGroup作为父控件包含多个View控件

自定义控件的一般步骤:

  1. 自定义属性
  2. 在自定义的构造函数中获取自定义属性
  3. 重写onMeasure()方法,对控件进行测量
  4. 重写onDraw()方法对控件进行绘制
  5. 重写onLayout方法,进行定位,即在父控件中的位置

我们来简单的看一个自定义控件是如何实现的:

1.自定义资源文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomTitleView">
        <attr name="titleText" format="string"/>
        <attr name="titleColor" format="color"/>
        <attr name="titleTextSize" format="dimension"/>
    </declare-styleable>
</resources>
  • string:文本数据
  • color:颜色数据
  • dimension:距离数据
  • reference:图片数据
  • 缩放类型数据,如下
<attr name="imageScaleType">
            <enum name="fillXY" value="1"/>
            <enum name="center" value="0"/>
 </attr>
  • integer:数字类型

2.新建类继承自View

public class CustomTitleView extends View {
    private String mTitileText;//文本
    private int mTitleColor;//文本颜色
    private int mTitleSize;//文本大小
    //绘制时控制文本绘制的范围
    private Rect mBound;
    //画笔
    private Paint mpaint;
    public CustomTitleView(Context context) {
    	//调用二参数构造
        this(context,null);
    }

    public CustomTitleView(Context context, AttributeSet attrs) {
    	//调用三参数构造
        this(context,attrs,0);
    }

    /**
     * 获取我自定义样式属性
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyleAttr, 0);//得到自定义属性
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {//遍历自定义属性
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.CustomTitleView_titleText:
                    mTitileText = a.getString(attr);
                    break;
                case R.styleable.CustomTitleView_titleColor:
                    //设置默认颜色为黑色
                    mTitleColor = a.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.CustomTitleView_titleTextSize:
                    //设置字体的默认大小为16sp
                    mTitleSize = a.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,16,getResources().getDisplayMetrics()));
                    break;
            }
        }
        a.recycle();
        mpaint = new Paint();
        mpaint.setTextSize(mTitleSize);//绘制文字
        mBound = new Rect();
        mpaint.getTextBounds(mTitileText,0,mTitileText.length(),mBound);//绘制文字的长度
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		/** 
         * 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式 
         */  
        //得到宽度的测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		//得到宽度
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		//得到高度的测量模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		//得到高度
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int heigth;
        if (widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        }else {
            mpaint.setTextSize(mTitleSize);
            mpaint.getTextBounds(mTitileText,0,mTitileText.length(),mBound);
            float textWidth = mBound.width();
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            width = desired;
        }

        if (heightMode == MeasureSpec.EXACTLY){
            heigth = heightSize;
        }else {
            mpaint.setTextSize(mTitleSize);
            mpaint.getTextBounds(mTitileText,0,mTitileText.length(),mBound);
            float textHeight = mBound.height();
            int desired = (int)(getPaddingTop()+textHeight+getPaddingBottom());
            heigth = desired;
        }
        setMeasuredDimension(width,heigth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mpaint.setColor(Color.YELLOW);//设置画笔的颜色
        //绘制一个矩形
        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mpaint);
		//设置画笔颜色
        mpaint.setColor(mTitleColor);
        //绘制文字
        canvas.drawText(mTitileText,getWidth()/2-mBound.width()/2,getHeight()/2+mBound.height()/2,mpaint);
    }
}

相关类讲解

  • Theme
    • 此类保存特定主题的当前属性值。换句话说,主题是资源属性的一组值;这些值与TypedArray结合使用以解析得到相应属性的值。
      • obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)返回一个TypedArray
  • TypedArray
    • 使用 obtainStyledAttributes(AttributeSet, int[], int, int) or obtainAttributes(AttributeSet, int[])得到的值数组的容器。确保在完成它们后调用recycle()。可以将自定义的属性放于此容器内,然后通过其方法去进行获取相应的属性值
    • AttributeSet也是能获得自定义属性值的,但获得方式不方便,在遇到@修饰的属性时,还要对其进行解析,而TypeArray是帮我们简化了这个过程.
  • TypedValue
    • 一个动态数据类型的集合,与Resources一起使用保存资源值.
    • 其中的方法大多使用了将复杂数据转换为简单的数值.
  • getResources().getDisplayMetrics()
    • 用来获得当前显示度量标准.
  • MeasureSpec
    • MeasureSpec封装了从父节点传递给子节点的布局要求。每个MeasureSpec表示宽度或高度的要求。MeasureSpec包括大小和模式。
    • 模式有
      • UNSPECIFIED(未确定):父控件没有对子控件施加任何约束。子控件可以是任何它想要的大小。
      • EXACTLY(准确):父控件已确定子控件的确切大小。无论子控件想要多大,它都将被给予父类指定的大小。一般为MATCH_PARENT
      • AT_MOST(最多):子控件可以大到它想要达到的指定大小。一般为WRAP_CONTENT
  • Paint
    • Paint类保存着有关如何绘制几何图形,文本和位图的样式与颜色的信息。可以通过此类进行绘制文字等内容
    • 部分相关方法:
返回值类型 方法名 功能
void setARGB(int a,int r,int g,int b) 设置绘制的颜色,a代表透明度,r,g,b代表颜色值
void setAlpha(int a) 设置绘制图形的透明度。
void setColor(int color) 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色.
void setAntiAlias(boolean aa) 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢.
void setDither(boolean dither) 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰
void setFilterBitmap(boolean filter) 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示 速度,本设置项依赖于dither和xfermode的设置
MaskFilter setMaskFilter(MaskFilter maskfilter) 设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等
ColorFilter setColorFilter(ColorFilter colorfilter) 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
PathEffect setPathEffect(PathEffect effect) 设置绘制路径的效果,如点画线等
Shader setShader(Shader shader) 设置图像效果,使用Shader可以绘制出各种渐变效果
void setShadowLayer(float radius ,float dx,float dy,int color) 在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色
void setStyle(Paint.Style style) 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE;Style.FILL: 实心 STROKE:空心;FILL_OR_STROKE:同时实心与空心
void setStrokeCap(Paint.Cap cap) 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式 .Cap.ROUND,或方形样式Cap.SQUARE
void setSrokeJoin(Paint.Join join) 设置绘制时各图形的结合方式,如平滑效果等
void setStrokeWidth(float width) 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度
Xfermode setXfermode(Xfermode xfermode) 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果
void setFakeBoldText(boolean fakeBoldText) 模拟实现粗体文字,设置在小字体上效果会非常差
void setSubpixelText(boolean subpixelText) 设置该项为true,将有助于文本在LCD屏幕上的显示效果.帮助setFlags(),设置或清除SUBPIXEL_TEXT_FLAG位
void setTextAlign(Paint.Align align) 设置绘制文字的对齐方向
void setTextScaleX(float scaleX) 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果
void setTextSize(float textSize) 设置绘制文字的字号大小
void setTextSkewX(float skewX) 设置斜体文字,skewX为倾斜弧度
Typeface setTypeface(Typeface typeface) 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
void setUnderlineText(boolean underlineText) 设置带有下划线的文字效果
void setStrikeThruText(boolean strikeThruText) 设置带有删除线的效果
  • Rect
    • Rect为矩形保存四个整数坐标。矩形由其4个边的坐标(左,上,右下)表示。这些字段可以直接访问。使用width()和height()来检索矩形的宽度和高度。注意:大多数方法不检查是否正确排序坐标.
    • 相关方法
返回值类型 方法名 功能
final int centerX() 获取矩阵中心点(x,y)
final int centerY()  
boolean contains(int x, int y) 是否包含(x,y)点Returns true if (x,y) is inside the rectangle.
boolean contains(int left, int top, int right, int bottom) 是否包含(int left, int top, int right, int bottom)矩阵..
boolean contains(Rect r) 是否包含矩形r
int describeContents() Parcelable接口方法
boolean equals(Object obj) Compares this instance with the specified object and indicates if they are equal.(指示一些其他对象是否等于此)
final float exactCenterX() 该方法和CenterX()类似,只是该方法精确度比它高(返回float类型)
final float exactCenterY()  
String flattenToString() Return a string representation of the rectangle in a well-defined format.(以明确定义的格式返回矩形的字符串表示形式。)
final int height() 获得矩形的高度
void inset(int dx, int dy) Inset the rectangle by (dx,dy).(安坐标插入矩形)
boolean intersect(Rect r) 如果指定的矩形与此矩形相交,则返回true并将此矩形设置为该交点,否则返回false,不改变此矩形。
boolean intersect(int left, int top, int right, int bottom) 如果左,上,右,下指定的矩形与此矩形相交,则返回true并将此矩形设置为该交点,否则返回false,不改变此矩形。
boolean intersects(int left, int top, int right, int bottom) 判断两矩形是否相交
static boolean intersects(Rect a, Rect b) 同上
final boolean isEmpty() 判断这个矩形是否存在 (left >= right or top >= bottom)
void offset(int dx, int dy) 该矩阵在x轴和y轴分别发生的偏移量(很有用,可以上下移动矩阵)Offset the rectangle by adding dx to its left and right coordinates, and adding dy to its top and bottom coordinates.
void offsetTo(int newLeft, int newTop) 保持矩阵的宽高,矩阵的左上角偏移到(newLeft,newTop)该点Offset the rectangle to a specific (left, top) position, keeping its width and height the same.
void readFromParcel(Parcel in) Set the rectangle’s coordinates from the data stored in the specified parcel.(从存储在指定包裹中的数据,设置矩形的坐标。)
void set(int left, int top, int right, int bottom) 将矩形坐标设置为指定的值。
void set(Rect src) 将坐标从src复制到这个矩形。
void setEmpty() 清除该矩形,即把坐标设置为(0,0,0,0)
boolean setIntersect(Rect a, Rect b) 如果矩形a和矩形b相交,返回true并将此矩形设置为该交点,否则返回false,并且不更改此矩形。
void sort() (如果有翻转,交换顶部/底部或左/右)Swap top/bottom or left/right if there are flipped (i.e.
String toShortString() 以紧凑形式返回矩形的字符串表示形式。
static Rect unflattenFromString(String str) 从flattenToString()返回的表单的字符串返回一个Rect,如果字符串不是该表单,则返回null。
void union(int left, int top, int right, int bottom)更新此Rect以包围自身和指定的矩形。  
void union(Rect r) 同上
void union(int x, int y) 更新此Rect以包围自身和[x,y]坐标。
final int width() 获取该矩形的宽度
void writeToParcel(Parcel out, int flags) 将此矩形写入指定的包裹。
  • Canvas
    • 画布类,可以绘制不同的形状

进阶

自定义一个可以显示图片的控件

public class CircleImage extends View {
    private String titleText;
    private int titleColor;
    private int titleSize;
    private Bitmap image;
    private int scraleType;
    private Paint paint;
    private Rect rect;
    private Rect titleBounds;
    private int width;
    private int height;

    public CircleImage(Context context) {
        this(context,null);
    }

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

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleImage, defStyleAttr, 0);
        int count = typedArray.getIndexCount();
        for (int i = 0; i < count; i++) {
            int index = typedArray.getIndex(i);
            switch (index) {
                case R.styleable.CircleImage_titleText:
                    titleText = typedArray.getString(index);
                    break;
                case R.styleable.CircleImage_titleTextColor:
                    titleColor = typedArray.getColor(index,0);
                    break;
                case R.styleable.CircleImage_titleTextSize:
                    titleSize = typedArray.getDimensionPixelSize(index, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,16,getResources().getDisplayMetrics()));
                    break;
                case R.styleable.CircleImage_image:
                    image = BitmapFactory.decodeResource(getResources(),typedArray.getResourceId(index,0));
                    break;
                case R.styleable.CircleImage_imageScaleType:
                    scraleType = typedArray.getInt(index,0);
                    break;
            }
        }
        typedArray.recycle();
        paint = new Paint();
        rect = new Rect();//图片区域
        titleBounds = new Rect();//文本区域
        paint.setTextSize(titleSize);//设置文本大小
        paint.getTextBounds(titleText,0,titleText.length(),titleBounds);//计算绘制的文本的范围.
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        }else {
            int desirtByImg = getPaddingLeft() + getPaddingRight() + image.getWidth();
            int desirtByTitle = getPaddingRight() + getPaddingLeft() + titleBounds.width();
            if (widthMode == MeasureSpec.AT_MOST){
                int max = Math.max(desirtByImg, desirtByTitle);
                width = Math.min(max,widthSize);
            }
        }

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        }else {
            int desirt = getPaddingBottom() + getPaddingTop() + image.getHeight()+titleBounds.height();
            if (heightMode == MeasureSpec.AT_MOST){
                height = Math.min(desirt,heightSize);
            }
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStrokeWidth(4);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.CYAN);
        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),paint);

        rect.left = getPaddingLeft();
        rect.top = getPaddingTop();
        rect.right = width-getPaddingRight();
        rect.bottom = height - getPaddingBottom();

        paint.setColor(titleColor);
        paint.setStyle(Paint.Style.FILL);

        if (titleBounds.width() > width){
            TextPaint textPaint = new TextPaint(paint);
            String msg = TextUtils.ellipsize(titleText, textPaint, (float) width - getPaddingLeft() - getPaddingRight(), TextUtils.TruncateAt.END).toString();
            canvas.drawText(msg,getPaddingLeft(),height - getPaddingBottom(),paint);
        }else {
            canvas.drawText(titleText,width/2-titleBounds.width()*1.0f/2,height - getPaddingBottom(),paint);
        }

        rect.bottom-=titleBounds.height();
        if (scraleType == 1){
            canvas.drawBitmap(image,null,rect,paint);
        }else {
            rect.left = width/2-image.getWidth()/2;
            rect.right = width/2+image.getWidth()/2;
            rect.top = (height-titleBounds.height())/2 - image.getHeight()/2;
            rect.bottom = (height-titleBounds.height())/2+image.getHeight()/2;

            canvas.drawBitmap(image,null,rect,paint);
        }
    }
}

自定义圆形进度条

效果如下:

加载控件

	public class CustomProgessBar extends View {
    private int firstColor;
    private int secondColor;
    private int cricleWidth;
    private int speed;
    private Paint paint;
    private int progress;
    private boolean isNext = false;

    public CustomProgessBar(Context context) {
        this(context,null);
    }

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

    public CustomProgessBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomProgessBar, defStyleAttr, 0);
        int count = a.getIndexCount();
        for (int i = 0; i < count; i++) {
            int index = a.getIndex(i);
            switch (index) {
                case R.styleable.CustomProgessBar_firstColor:
                    firstColor = a.getColor(index, Color.GREEN);
                    break;
                case R.styleable.CustomProgessBar_secondColor:
                    secondColor = a.getColor(index,Color.BLACK);
                    break;
                case R.styleable.CustomProgessBar_circleWidth:
                    cricleWidth = a.getDimensionPixelSize(index, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,20,getResources().getDisplayMetrics()));
                    break;
                case R.styleable.CustomProgessBar_speed:
                    speed = a.getInt(index,20);
                    break;
            }
        }
        a.recycle();
        paint = new Paint();
        new Thread(){
            @Override
            public void run() {
                while (true){
                    progress++;
                    if (progress == 360){
                        progress = 0;
                        if (!isNext){
                            isNext = true;
                        }else {
                            isNext = false;
                        }
                    }
                    postInvalidate();
                    try {
                        Thread.sleep(speed);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
        //圆心x轴坐标
        int center = getWidth() / 2;
        //半径
        int radius = center - cricleWidth / 2;
        //圆环宽度
        paint.setStrokeWidth(cricleWidth);
        //消除锯齿
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        RectF oval = new RectF(center - radius, center - radius, center + radius, center + radius);
        if (!isNext){
            paint.setColor(firstColor);
            canvas.drawCircle(center,center,radius,paint);
            paint.setColor(secondColor);
            canvas.drawArc(oval,-90,progress,false,paint);
        }else {
            paint.setColor(secondColor);
            canvas.drawCircle(center,center,radius,paint);
            paint.setColor(firstColor);
            canvas.drawArc(oval,-90,progress,false,paint);
        }

    }
}

差值器InterPolator

差值器主要定义动画的变化率,这准许基本动画的效果加速,减速,重复等.