Android 自定义控件

前言:

在日常的Android开发中会经常和控件打交道,有时Android提供的控件未必能满足业务的需求,这个时候就需要我们实现自定义一些控件,这里将介绍自定义控件的原理和实现方法。

参考文章:

自定义控件要求:

  • 应当遵守Android标准的规范(命名,可配置,事件处理等)。
  • 在XML布局中可配置控件的属性。
  • 对交互应当有合适的反馈,比如按下,点击等。
  • 具有兼容性, Android版本很多,应该具有广泛的适用性。

创建自定义控件步骤:

下面以创建一个圆形百分比控件为例,讲解自定义控件的实现过程。

第一步:声明属性

在res/values文件下添加一个attrs.xml文件,如果项目比较大的话,会导致attrs.xml代码相当庞大,这时可以根据相应的功能模块起名字,方便查找,例如:登录模块相关attrs_login.xml

使用<declare-styleable name="PercentView"> </declare-styleable>来定义一个属性集合,name就是属性集合的名字,这个名字一定要起的见名知意。

然后就是定义属性值了,通过<attr name="textColor" format="color" />方式定义属性值,属性名字同样也要起的见名知意,format表示这个属性的值的类型,类型有以下几种:

  • reference:引用资源
  • string:字符串
  • color:颜色
  • boolean:布尔值
  • dimension:尺寸值
  • float:浮点型
  • integer:整型
  • fraction:百分数
  • enum:枚举类型
  • flag:位或运算

基于上面的要求,我们可以定义一下百分比控件属性:

<declare-styleable name="PercentView">
        <attr name="percent_circle_gravity"><!--圆形绘制的位置-->
            <flag name="left" value="0" />
            <flag name="top" value="1" />
            <flag name="center" value="2" />
            <flag name="right" value="3" />
            <flag name="bottom" value="4" />
        </attr>
        <attr name="percent_circle_radius" format="dimension" /><!--圆形半径-->
        <attr name="percent_circle_progress" format="integer" /><!--当前进度值-->
        <attr name="percent_progress_color" format="color" /><!--进度显示颜色-->
        <attr name="percent_background_color" format="color" /><!--圆形背景色-->
    </declare-styleable>

第二步:在布局中引用

使用xmlns:fk="http://schemas.android.com/apk/res-auto"为属性集设置一个属性集名称,我这里用的fk,建议在真正的项目中使用项目的缩写,比如微信可能就是使用wx。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:fk="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <fk.androiddemo_035.PercentView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_margin="10dp"
        android:background="@color/red"
        android:padding="10dp"
        fk:percent_background_color="@color/gray"
        fk:percent_circle_gravity="left"
        fk:percent_circle_progress="30"
        fk:percent_circle_radius="50dp"
        fk:percent_progress_color="@color/blue" />

</LinearLayout>

第三步:编写继承自View的子类

一、View结构原理

Android系统的视图结构的设计也采用了组合模式,即View作为所有图形的基类,Viewgroup对View继承扩展为视图容器类。

View定义了绘图的基本操作,基本操作由三个函数完成:measure()、layout()、draw(),其内部又分别包含了onMeasure()、onLayout()、onDraw()三个子方法。具体操作如下:

1、measure操作

measure操作主要用于计算视图的大小,即视图的宽度和长度。在view中定义为final类型,要求子类不能修改。measure()函数中又会调用onMeasure()函数,视图大小的将在这里最终确定,也就是说measure只是对onMeasure的一个包装,子类可以覆写onMeasure()方法实现自己的计算视图大小的方式,并通过setMeasuredDimension(width, height)保存计算结果。

2、layout操作

layout操作用于设置视图在屏幕中显示的位置。在view中定义为final类型,要求子类不能修改。layout()函数中有两个基本操作:

(1)setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,该函数用于将这些参数保存起来;

(2)onLayout(),在View中这个函数什么都不会做,提供该函数主要是为viewGroup类型布局子视图用的;

3、draw操作

draw操作利用前两部得到的参数,将视图显示在屏幕上,到这里也就完成了整个的视图绘制工作。子类也不应该修改该方法,因为其内部定义了绘图的基本操作:

(1)绘制背景;

(2)如果要视图显示渐变框,这里会做一些准备工作;

(3)绘制视图本身,即调用onDraw()函数。在view中onDraw()是个空函数,也就是说具体的视图都要覆写该函数来实现自己的显示(比如TextView在这里实现了绘制文字的过程)。而对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的,其包含了多个子view,而子View已经实现了自己的绘制方法,因此只需要告诉子view绘制自己就可以了,也就是下面的dispatchDraw()方法;

(4)绘制子视图,即dispatchDraw()函数。在view中这是个空函数,具体的视图不需要实现该方法,它是专门为容器类准备的,也就是容器类必须实现该方法;

(5)如果需要(应用程序调用了setVerticalFadingEdge或者setHorizontalFadingEdge),开始绘制渐变框;

(6)绘制滚动条;

从上面可以看出自定义View需要最少覆写onMeasure()和onDraw()两个方法。

二、View类的构造方法

创建自定义控件的3种主要实现方式:

  1. 继承已有的控件来实现自定义控件: 主要是当要实现的控件和已有的控件在很多方面比较类似, 通过对已有控件的扩展来满足要求。
  2. 通过继承一个布局文件实现自定义控件,一般来说做组合控件时可以通过这个方式来实现。注意此时不用onDraw方法,在构造函数中通过Inflater加载自定义控件的布局文件,再addView(view),自定义控件的图形界面就加载进来了。
  3. 通过继承view类来实现自定义控件,使用GDI绘制出组件界面,一般无法通过上述两种方式来实现时用该方式。

三、获取自定义属性

每一个属性集合编译之后都会对应一个styleable对象,通过styleable对象获取TypedArray typedArray,然后通过键值对获取属性值,这点有点类似SharedPreference的取法。

TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.PercentView);
if (typedArray != null) {backgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.GRAY);
    progressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.BLUE);
    radius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
    progress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
    gravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
    typedArray.recycle();
}

下面是整个PrecentView类代码:

package fk.androiddemo_035;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Created by FK on 2017/2/15.
 */

public class PercentView extends View {
    private final static String TAG = PercentView.class.getSimpleName();
    private Paint mPaint;
    private int backgroundColor = Color.GRAY;
    private int progressColor = Color.BLUE;
    private float radius;
    private int progress;
    private float centerX = 0;
    private float centerY = 0;
    public static final int LEFT = 0;
    public static final int TOP = 1;
    public static final int CENTER = 2;
    public static final int RIGHT = 3;
    public static final int BOTTOM = 4;
    private int gravity = CENTER;
    private RectF rectF;  //用于定义的圆弧的形状和大小的界限

    public PercentView(Context context) {
        super(context);
        init();
    }

    public PercentView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initParams(context, attrs);
        init();
    }

    public PercentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initParams(context, attrs);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        rectF = new RectF();
    }

    private void initParams(Context context, AttributeSet attrs) {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        rectF = new RectF();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentView);
        if (typedArray != null) {
            backgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.GRAY);
            progressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.BLUE);
            radius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
            progress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
            gravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
            typedArray.recycle();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(backgroundColor);
        // FILL填充, STROKE描边,FILL_AND_STROKE填充和描边
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        canvas.drawCircle(centerX, centerY, radius, mPaint);
        mPaint.setColor(progressColor);

        double percent = progress * 1.0 / 100;
        int angle = (int) (percent * 360);
        canvas.drawArc(rectF, 270, angle, true, mPaint);  //根据进度画圆弧
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        Log.e(TAG, "onMeasure--widthMode-->" + widthMode);
        switch (widthMode) {
            case MeasureSpec.EXACTLY://
                break;
            case MeasureSpec.AT_MOST:
                break;
            case MeasureSpec.UNSPECIFIED:
                break;
        }
        Log.e(TAG, "onMeasure--widthSize-->" + widthSize);
        Log.e(TAG, "onMeasure--heightMode-->" + heightMode);
        Log.e(TAG, "onMeasure--heightSize-->" + heightSize);
        int with = getWidth();
        int height = getHeight();
        Log.e(TAG, "onDraw---->" + with + "*" + height);
        centerX = with / 2;
        centerY = with / 2;
        switch (gravity) {
            case LEFT:
                centerX = radius + getPaddingLeft();
                break;
            case TOP:
                centerY = radius + getPaddingTop();
                break;
            case CENTER:
                break;
            case RIGHT:
                centerX = with - radius - getPaddingRight();
                break;
            case BOTTOM:
                centerY = height - radius - getPaddingBottom();
                break;
        }
        float left = centerX - radius;
        float top = centerY - radius;
        float right = centerX + radius;
        float bottom = centerY + radius;
        rectF.set(left, top, right, bottom);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.e(TAG, "onLayout");
    }
}

运行结果

根据不同的配置显示的两种效果

这里写图片描述

源码下载

地址:http://download.csdn.net/detail/youmingyu/9755075

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章