Android知识进阶(遥远的重头开始)自定义控件

Android-自定义View-自定义属性

2019-07-12  本文已影响5人  MonkeyLei

上一篇Android-自定义View-onDraw方法起步我们已经基本了解了绘制。不过有点不太灵活,大部分小白作者都是这样开场哈!嘿嘿....

比如最简单的就是我们圆圈的半径应该有xml里面直接指定,比如绘制的颜色应该xml配置好,然后我们绘制时获取半径和颜色就可以进行绘制了。这样大大增加了灵活性,好伐!

So,涉及到如何自定义属性以及获取属性。

一、自定义属性

主要涉及到两个 attrs.xml 、 styles.xml。网上有很多关于属性主题的的详细说明。我们只简单关心两点:

1. attrs.xml - 用来定义各种自定义属性,比如显示的文本,绘制的颜色,绘制的半径,这些文本格式就是字符串(string),半径就是整数(integer), 绘制的颜色就是字符("#ffffff"),然后等等属性

<?xml version="1.0" encoding="utf-8"?>
<resources> 
   <!--声明我们的属性,名称为radius,取值类型为尺寸类型(dp,px等)-->
   <attr name="radius" format="dimension"></attr>
</resources>

注意: format蛮重要的,关于具体格式后面找到官方的链接了我们专门总结下,暂时知道几个就行

2. styles.xml - 用来给attrs.xml定义的自定义属性进行赋值 - MyTextView01

  <resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    <style name="MyTextView01">
        <item name="radius">50</item>
    </style>
</resources>

二、获取属性

1. xml里面设置属性或者style

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <me.heyclock.hl.customcopy.MyTextView01
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:radius="50dp"
        style="@style/MyTextView01"
        android:background="@color/colorPrimary" />

</android.support.constraint.ConstraintLayout>

2.根据AttributeSet类的官方文档,可以获取到属性个数,进而可以进行遍历获取相关属性和值

   public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.context = context;
        ///< 获取自定义属性值
        for (int i = 0; i < attrs.getAttributeCount(); ++i){
            Log.e("attrs", "" + i + "-name=" + attrs.getAttributeName(i) + " value=" + attrs.getAttributeValue(i));
        }
        ///< 1\. 做一些绘制初始化
        canvas = new Canvas();  ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
        paint = new Paint();
        paint.setColor(Color.parseColor("#F50808"));
    }

看结果:

image

2. 1 由于我们直接调用的是

image

而我们的布局文件里面是 - 有资源ID的引用

image

2. 2 所以上面只能拿到资源Id,我们还需要根据Id进一步获取对应的值:

        int colorId =  attrs.getAttributeResourceValue(0, -1);
        Log.e("attrs", "colorId= " + colorId);
        Log.e("attrs", "color= " + Integer.toHexString(getResources().getColor(colorId)));

看看是不是就可以获取了呀呀....真的是帅的一笔....

image

2. 3 这个时候是不是还有别的方法了 - **TypedArray **?我们经常看别人自定义的控件里面获取属性基本都是酱紫写的:

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);

        String text = ta.getString(R.styleable.test_testAttr);
        int textAttr = ta.getInteger(R.styleable.test_text, -1);
        ta.recycle();

就是这个TypedArray,这个时候我们需要定义一个styleable

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="MyTextView01">
      <attr name="radius" format="dimension"></attr>
   </declare-styleable>
</resources>

布局一下:activity_main.xml

 <me.heyclock.hl.customcopy.MyTextView01
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        app:radius="50dp" />

然后我们就可以利用TypedArray获取属性值了

        ///< TypedArray的方式
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView01);
        ///< getDimension() getDimensionPixelOffset() getDimensionPixelSize()
        ///  --这三个方法都是根据DisplayMetrics获取相应的值,不同在于方法1直接保存float型数据,方法2直接对float取整,方法3对float小数先四舍五入后取整。
        radius = ta.getDimensionPixelOffset(R.styleable.MyTextView01_radius, 50);
        Log.e("attrs", "像素值 radius= " + radius);
        ta.recycle();
image

2. 4 然后之前的styles呢?? 我们可以把属性值赋值在styles.xml里面定义:

styles.xml

  <style name="MyTextView01">
        <item name="radius">150dp</item>
    </style>

然后布局里面使用:

  <me.heyclock.hl.customcopy.MyTextView01
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@color/colorPrimary"
            style="@style/MyTextView01" />

--那右怎么获取style的属性了?也是一样的获取呀。。style只是给其赋值而已....

image

Nice...至少又加深了对属性、style的认识。

2. 4. 1 我们属性是不是也可以app:radius="50dp"这样赋值 ?

  <me.heyclock.hl.customcopy.MyTextView01
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@color/colorPrimary"
            style="@style/MyTextView01"
            app:radius="50dp" />  

疑问:这个时候就会有疑问呢?到底是styles.xml里面赋值管用还是app:radius里面管用?

根据结果发现app:radius管用。。。

image

关于这个有个优先级: AttributeSet(layout) > defStyleAttr(@AttrRes主题可配置样式)> defStyleRes(@StyleRes默认样式)> NULL(主题中直接指定)

Style我们暂时不用这种方式哈!先用app的方式进行属性赋值,将之前的demo扩展一下。

activity_main.xml

   <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

        <me.heyclock.hl.customcopy.MyTextView01
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@color/colorPrimary"
            style="@style/MyTextView01"
            app:radius="50dp" />

</android.support.constraint.ConstraintLayout>

另外attrs.xml里面我们新增宽高的属性名称,方便我们获取宽高设置

 <?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="MyTextView01">
      <attr name="radius" format="dimension"></attr>
      <attr name="ccolor" format="string"></attr>
      <attr name="android:layout_width"/>
      <attr name="android:layout_height"/>
   </declare-styleable>
</resources>

另外我们内部的宽高计算,中心点的计算,以及红色区域的范围,我们均采用动态计算,不再写死:

    int minX = (width - radius * 2)/2;
        int maxX = width/2 + radius;
        int minY = (height - radius * 2)/2;
        int maxY = height/2 + radius;
canvas.drawCircle(width/2, height/2,
                radius + changeRadius, paint);

最后完整的文件 MyTextView01.java

  package me.heyclock.hl.customcopy;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import java.util.Timer;
import java.util.TimerTask;

/*
 *@Description: 自定义绘制文本
 *@Author: hl
 *@Time: 2018/10/12 9:37
 */
public class MyTextView01 extends View {
    /* 官方文档:
        https://developer.android.google.cn/reference/android/graphics/Canvas
        https://developer.android.google.cn/reference/android/graphics/Paint
    */
    private Context context;///< 上下文
    private Canvas canvas;  ///< 画布
    private Paint paint;    ///< 画笔

    ///< 做红色点击区域限制
    private boolean bIsDownInRedRegion = false;
    ///< 定时刷新
    private Timer timer = null;
    ///< 圆圈半径
    private int radius;
    ///< 圆圈颜色
    private String color;
    ///< 控件宽度和高度
    private int width;
    private int height;

    /**
     * 刷新绘制+增量变化
     */
    private static final int STEP_RADIUS = 10;  ///< 每次半径增加10
    private int changeRadius = 0;               ///< 变化量记录,达到50时则开始减;达到0就开始增加
    private boolean addFlag = true;             ///< 标记是否增加增量

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

    public MyTextView01(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, 0, 0);
    }

    public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.context = context;
        ///< 获取自定义属性值
        for (int i = 0; i < attrs.getAttributeCount(); ++i){
            Log.e("attrs", "" + i + "-name="
                    + attrs.getAttributeName(i)
                    + " value=" + attrs.getAttributeValue(i));
        }
        int colorId =  attrs.getAttributeResourceValue(0, -1);
        Log.e("attrs", "colorId= " + colorId);
        Log.e("attrs", "color= " + Integer.toHexString(getResources().getColor(colorId)));

        ///< TypedArray的方式
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView01);
        ///< getDimension() getDimensionPixelOffset() getDimensionPixelSize()
        ///  --这三个方法都是根据DisplayMetrics获取相应的值,不同在于方法1直接保存float型数据,方法2直接对float取整,方法3对float小数先四舍五入后取整。
        radius = ta.getDimensionPixelOffset(R.styleable.MyTextView01_radius, 50);
        color = ta.getString(R.styleable.MyTextView01_ccolor);
        width = ta.getDimensionPixelOffset(R.styleable.MyTextView01_android_layout_width, 200);
        height = ta.getDimensionPixelOffset(R.styleable.MyTextView01_android_layout_height, 200);

        ///< 做一个兼容,如果半径超过了控件宽或者高
        int minWH = width;
        if (width > height){
            minWH = height;
        }
        if ((radius * 2) > minWH){
            radius = minWH / 2;
        }

        Log.e("attrs", "像素值 radius= " + radius);
        Log.e("attrs", "color= " + color);
        Log.e("attrs", "width= " + width);
        Log.e("attrs", "height= " + height);
        ta.recycle();

        ///< 1\. 做一些绘制初始化
        canvas = new Canvas();  ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
        paint = new Paint();
        paint.setColor(Color.parseColor(color));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);
        ///< 2.进行绘制
        ///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
        canvas.drawCircle(width/2, height/2,
                radius + changeRadius, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("test", "getX=" + event.getX());
        Log.e("test", "getY=" + event.getY());
        //Log.e("test", "getRawX=" + event.getRawX());
        //Log.e("test", "getRawY=" + event.getRawY());

        int x = (int) event.getX();
        int y = (int) event.getY();

        ///< 控件大小是: dp2px(context, 200) * dp2px(context, 200)
        ///< 圆心坐标是:  dp2px(context, 100) * dp2px(context, 100)
        ///< 圆半径是:    dp2px(context, 50)
        ///< 所以点击区域就是左上角范围(dp2px(context, 50), dp2px(context, 50))
        ///<                右下角范围:(dp2px(context, 150), dp2px(context, 150))
        int minX = (width - radius * 2)/2;
        int maxX = width/2 + radius;
        int minY = (height - radius * 2)/2;
        int maxY = height/2 + radius;
        Log.e("test", "x=" + x);
        Log.e("test", "y="+ y);
        Log.e("test", "minX=" + minX);
        Log.e("test", "maxX="+ maxX);
        Log.e("test", "minY=" + minY);
        Log.e("test", "maxY="+ maxY);
        Log.e("test", "event.getAction()=" + event.getAction());

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (x >= minX && x <= maxX &&
                    y >= minY && y <= maxY){
                    bIsDownInRedRegion = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                if (bIsDownInRedRegion){
                    bIsDownInRedRegion = false;

                    if (x >= minX && x <= maxX &&
                        y >= minY && y <= maxY){
                        ///< 抬手时我们就可以启动定时器进行绘制刷新了
                        Log.e("test", "红色区域点击了呀,sb");
                        if (null == timer){
                            timer = new Timer();
                            timer.schedule(new TimerTask() {
                                @Override
                                public void run() {
                                    ///< Handler也行
                                    ((Activity)context).runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            updateDraw();
                                        }
                                    });
                                }
                            }, 0, 100);
                        }else{
                            timer.cancel();
                            timer = null;
                        }
                    }
                }
                break;
        }
        return true;
    }

    /**
     * 刷新绘制+增量变化
     */
    private void updateDraw(){
        changeRadius = addFlag ? (changeRadius += STEP_RADIUS) : (changeRadius -= STEP_RADIUS);
        if (changeRadius > 50){
            addFlag = false;
        }else if (changeRadius < 0){
            addFlag = true;
        }
        invalidate();
    }

    /**
     * dp转px
     * @param dp
     * @return
     */
    public static int dp2px(Context context, int dp){
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
    }
}

其中半径做了一个兼容,怕超过屏幕宽高

        ///< 做一个兼容,如果半径超过了控件宽或者高
        int minWH = width;
        if (width > height){
            minWH = height;
        }
        if ((radius * 2) > minWH){
            radius = minWH / 2;
        }

Last

1\. attrs.xml里面的declare-styleable以及item,android会根据其在R.java中生成一些常量方便我们使用(aapt干的),本质上,我们可以不声明declare-styleable仅仅声明所需的属性即可。
2\. 我们在View的构造方法中,可以通过AttributeSet去获得自定义属性的值,但是比较麻烦,而TypedArray可以很方便的便于我们去获取。
3\. 我们在自定义View的时候,可以使用系统已经定义的属性。

自定义属性基本到这。后面遇到什么再深入和总结哈....喵喵!

上一篇下一篇

猜你喜欢

热点阅读