美文网首页
自定义View——实现1.圆

自定义View——实现1.圆

作者: 四喜汤圆 | 来源:发表于2019-05-27 20:02 被阅读0次

这种方法主要用于实现一些不规则的效果,一般需要重写onDraw()。采用这种方法需要自己支持wrap_content,并且padding也需要自己处理。

下面记录Android 开发艺术探索一书中的“自定义圆”示例的完成步骤。在自己的中心点,以宽/高的最小值为直径绘制一个红色的实心圆。

1. 重写 onDraw()

(1)重写原因
View 的 draw 过程中,在onDraw()中完成 View 自身内容的绘制,由于每个 View 的内容各不相同,所以在View类中该方法是一个空实现。自定义 View 时,需子类复写onDraw(),从而完成自身内容的绘制。

(2)具体实现
创建类CircleView继承View类,重写onDraw(),下面是CircleView#onDraw()的实现。

private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

/**
 * 在该方法中绘制View自身内容
 * @param canvas
 */
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    // 半径
    int radius = Math.min(width, height) / 2;
    // 画圆
    canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}

2. 考虑 margin 属性

margin 属性由父容器控制,因此不需要在CircleView中做特殊处理

3. 考虑 padding 属性

(1)padding 默认无法生效原因
直接继承自 View 和 ViewGroup 的控件,padding 默认是无法生效的,需要自己处理。

(2)处理 padding
针对padding的问题,也很简单,只要在绘制时考虑一下padding即可,在绘制时考虑 View 四周的 padding,从而做相应的调整。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingTop = getPaddingTop();
    int paddingBottom = getPaddingBottom();

    // getWidth():
    int width = getWidth() - paddingLeft - paddingRight;
    // getHeight():
    int height = getHeight() - paddingTop - paddingBottom;
    int radius = Math.min(height, width) / 2;
    canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.myapplication.diyview.MyCircle
        android:layout_width="match_parent"
        android:layout_margin="20dp"
        android:padding="20dp"
        android:background="#000000"
        android:id="@+id/diyview_activity_layout_circle"
        android:layout_height="100dp" />

</LinearLayout>
不带padding 带padding的圆

4. 考虑 wrap_content 属性

(1)问题描述
自定义 View 时,View 宽/高的wrap_content属性起不到自身应有的作用,而是起到和match_parent相同的作用。

  • wrap_content预期作用
    视图的宽/高被设定成刚好适应视图内容的最小尺寸

  • match_parent
    视图的宽/高被设定成充满父布局

(2)问题分析

从 View 的源码来看,在自定义 View 不进行任何处理时,wrap_content属性起到的作用就是和match_parent相同。我们对系统控件使用wrap_content属性有效果,其实还是控件内部处理过了

对单一 View 的测量过程通过调用View类的measure()进行,在measure()之前要得到测量规格MeasureSpec,其中普通 View (顶层 View 的暂且先不分析)的MeasureSpec的得到和父容器的MeasureSpec和 View 本身的布局参数LayoutParams相关,具体见下表。

摘自《Android开发艺术探索》

从上表可知

  • 当 View 的布局参数为wrap_content
    得到的 View 的测量模式都为AT_MOST,测量规格为parentSize,即父布局剩余空间

  • 当 View 的布局参数为match_parent
    得到的 View 的测量模式为AT_MOSTEXACTLY,测量规格为parentSize,即父布局剩余空间

所以说,在自定义 View 不做任何处理,默认的情况下,设置 View 宽/高的wrap_content是起不到应有的作用,而是起到和match_parent相同的效果,即都是充满父容器的剩余尺寸(parentSize)。

(3)解决方案
为了让wrap_content属性起到应有效果,自定义 View 时需重写onMeasure(),在该方法中对该属性做特殊处理。只需要指定一个wrap_content模式的默认宽/高即可,比如选择 400px 作为默认的宽高

@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);

    // 设置wrap_content的默认宽 / 高值
    // 默认宽/高的设定并无固定依据,根据需要灵活设置
    // 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
    int mWidth = 400;
    int mHeight = 400;

    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(mWidth, heightSize);
    } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(widthSize, mHeight);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.myapplication.diyview.MyCircle
        android:layout_width="wrap_content"
        android:layout_margin="20dp"
        android:padding="20dp"
        android:background="#000000"
        android:id="@+id/diyview_activity_layout_circle"
        android:layout_height="100dp" />

</LinearLayout>

具体问题具体分析,这里的固定宽高也可以通过函数调用来实现。这里固定宽高是为了便于分析,如果是自定义 TextView ,那么只需要根据文字的宽高计算控件的宽高,替代这里的固定宽高。

5. 提供自定义属性

(1)在 values 目录下创建自定义属性的 xml 文件

  • xml 文件名随意取,没什么限制。
  • <declare-styleable>声明自定义属性集CircleView
    在这个集合中定义自定义属性(自定义属性需指定nameformat两个属性),自定义属性的format有多种,可查阅文档。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--自定义属性集-->
    <declare-styleable name="CircleView">
        <!--自定义属性-->
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

(2)在 View 的构造函数中解析自定义属性的值并做相应处理

  • 加载自定义属性集合
  • 解析属性集合中的自定义属性
  • 将自定义属性应用到 View
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    // 加载自定义属性集合
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
    // 解析自定义属性:自定义属性的ID为CircleView_circle_color,即属性集合名_自定义属性名
    mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
    // 解析完自定义属性后,释放资源
    typedArray.recycle();
    init();
}

private void init() {
    mPaint.setColor(mColor);
}

(3)在布局文件中使用自定义属性

  • 为了使用自定义属性,需要在布局文件中添加schemas声明
xmlns:app="http://schemas.android.com/apk/res-auto"

app:是自定义属性的前缀,当然也可以换成其他名字,然后就可以在布局文件中使用自定义属性了。

  • 在布局文件中使用自定义属性

app:circle_color="@color/colorPrimary"

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <com.whut.mytest.diy_view.CircleView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:circle_color="@color/colorPrimary"/>
</LinearLayout>
自定义颜色

参考文献

手把手教你写一个完整的自定义View
Android开发艺术探索

完整代码

1.CircleView

public class CircleView extends View {
    private static final String TAG = "CircleView";
    // 在自己的中心点,以宽/高的最小值为直径绘制一个红色的实心圆
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

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

    public CircleView(Context context, AttributeSet attrs) {
        // 若使用自定义属性的话,此函数调用记得修改,原来是:super(context,attrs);
        this(context, attrs, 0);
        init();
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 加载自定义属性集合
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        // 解析自定义属性:自定义属性的ID为CircleView_circle_color,即属性集合名_自定义属性名
        mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.BLUE);
        Log.d(TAG, "CircleView()::mColor=" + mColor);
        // 解析完自定义属性后,释放资源
        typedArray.recycle();
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        // getWidth():
        int width = getWidth() - paddingLeft - paddingRight;
        // getHeight():
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(height, width) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, 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);

        // 设置wrap_content的默认宽 / 高值
        // 默认宽/高的设定并无固定依据,根据需要灵活设置
        // 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
        int mWidth = 400;
        int mHeight = 400;

        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
            getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, heightSize);
        } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(widthSize, mHeight);
        }
    }
}

2.自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <!--声明自定义属性集合CirCleView-->
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

3. Activity布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="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">

    <com.example.myapplication.diyview.CircleView
        android:id="@+id/diyview_activity_layout_circle"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000"
        android:padding="20dp"
        app:circle_color="@color/colorAccent" />

</LinearLayout>

相关文章

网友评论

      本文标题:自定义View——实现1.圆

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