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

作者: MonkeyLei | 来源:发表于2019-07-12 09:46 被阅读5次

    上一篇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的时候,可以使用系统已经定义的属性。
    

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

    相关文章

      网友评论

        本文标题:Android-自定义View-自定义属性

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