36、自定义控件详解(一)-- 自定义属性

一、自定义控件分类

1.1、原生控件拓展

修改原有控件我们只需要创建一个类继承该View(ViewGroup),再原有的逻辑上添加自己的实现即可。

a)文本框(TextView)默认是无法获取焦点的,想让它获取焦点,我们可以通过自定义控件,并重写isFocused来解决。

public class FocuseTextView extends TextView {
    public FocuseTextView(Context context) {
        this(context,null);
    }
    public FocuseTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    @ExportedProperty(category = "focus")
    public boolean isFocused() {
        //欺骗系统,误以为textview得到了焦点
        return true;
    }
}

b)绘制一个带矩形边框的TextView

public class MyTetView extends TextView {
    private Paint mPaint1;
    private Paint mpaint2;
    public MyTetView(Context context) {
        this(context,null);
    }

    public MyTetView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    private void initPaint() {
        mPaint1 = new Paint();
        mPaint1.setStyle(Paint.Style.FILL);
        mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
        
        mpaint2 = new Paint();
        mpaint2.setStyle(Paint.Style.FILL);
        mpaint2.setColor(Color.YELLOW);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        // 绘制外层矩形
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint1);
        // 绘制内层矩形
        canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mpaint2);
        canvas.save();
        super.onDraw(canvas);
        canvas.restore();
    }
}

1.2、自定义组合控件

(1) 组合原生控件

在自定义组合控件时,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件组合即可。

a) 我们首先制定好需要组合的控件,用它来达到我们想要的效果:

<RelativeLayout 
    android:id="@+id/rl_viewgroup"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView 
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:textSize="20sp"
        android:text="我是标题"/>
    <TextView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:layout_below="@id/tv_content"
        android:textColor="@android:color/darker_gray"
        android:text="我是未被选中的描述"/>
    <CheckBox 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_marginRight="10dp"/>
</RelativeLayout>

b) 在values目录下创建attrs.xml文件,并定义好属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name = "combinationView">
        <attr name = "title" format = "string"/>
        <attr name = "content" format = "string"/>
        <attr name = "focusable" format = "boolean"/>
    </declare-styleable>
</resources>

c) 创建自定义控件类继承自ViewGroup,并实现带attrs的构造函数,再使用TypeArray来获取属性:

public class TextViewCheckBox extends RelativeLayout {

    private TextView mTvTitle,mTvContent;
    private CheckBox mCbClick;
    private String mTitle,mContentOn,mContentOff;
    
    public TextViewCheckBox(Context context) {
        this(context,null);
    }
    public TextViewCheckBox(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化布局和控件
        View view = View.inflate(context, R.layout.ui_text_checkbox, this);
        mTvTitle = (TextView) view.findViewById(R.id.tv_title);
        mTvContent = (TextView) view.findViewById(R.id.tv_content);
        mCbClick = (CheckBox) view.findViewById(R.id.cb_click);
        
        // 将attrs.xml中定义的所有属性的值存储到TypeArray中
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.combinationView);
        mTitle = array.getString(R.styleable.combinationView_title);
        mContentOn = array.getString(R.styleable.combinationView_content_on);
        mContentOff = array.getString(R.styleable.combinationView_content_off);
        array.recycle();
        
        // 初始化子控件描述和状态
        if(mTitle != null){
            mTvTitle.setText(mTitle);
        }
        
        if(mContentOff != null){
            mTvContent.setText(mContentOff);
        }
    }
}

d) 暴露方法给调用者来设置描述和状态:

/**判断是否被选中*/
public boolean isChecked(){
    return mCbClick.isChecked();
}

/**设置选中的状态*/
public void setChecked(boolean isChecked){
    mCbClick.setChecked(isChecked);
    if(isChecked){
        mTvContent.setText(mContentOn);
    }else{
        mTvContent.setText(mContentOff);
    }
}

e) 在布局中引用该控件,引入名称空间,并设置自定义的属性。

<cn.legend.review.TextViewCheckBox
    android:id="@+id/tvc_textchecked"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    review:title="我是标题"
    review:content_on = "控件被选中"
    review:content_off = "控件没有选中"/>

注意:在使用自定义控件时需要引入名称空间:xmlns:review="http://schemas.android.com/apk/res/cn.legend.review"

如果想让控件响应事件的话,则直接重写事件即可,如果用到了wrap_content或match_parent则需要进行测量等操作。

1.3、自定义属性种类

1. reference:参考某一资源ID。

(1)属性定义:
        <declare-styleable name = "名称">
            <attr name = "background" format = "reference" />
        </declare-styleable>
(2)属性使用:
         <ImageView
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:background = "@drawable/图片ID"/>

2. color:颜色值

(1)属性定义:
        <declare-styleable name = "名称">
            <attr name = "textColor" format = "color" />
        </declare-styleable>
(2)属性使用:
        <TextView
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:textColor = "#00FF00"/>

3. boolean:布尔值

1)属性定义:
        <declare-styleable name = "名称">
            <attr name = "focusable" format = "boolean" />
        </declare-styleable>
(2)属性使用:
        <Button
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:focusable = "true"/>

4. dimension:尺寸值

(1)属性定义:
        <declare-styleable name = "名称">
            <attr name = "layout_width" format = "dimension" />
        </declare-styleable>
(2)属性使用:
        <Button
            android:layout_width = "42dip"
            android:layout_height = "42dip"/>

5. float:浮点值

(1)属性定义:
        <declare-styleable name = "AlphaAnimation">
            <attr name = "fromAlpha" format = "float" />
            <attr name = "toAlpha" format = "float" />
        </declare-styleable>
(2)属性使用:
        <alpha
           android:fromAlpha = "1.0"
           android:toAlpha = "0.7"/>

6. integer:整型值

(1)属性定义:
        <declare-styleable name = "AnimatedRotateDrawable">
            <attr name = "visible" />
            <attr name = "frameDuration" format="integer" />
            <attr name = "framesCount" format="integer" />
            <attr name = "pivotX" />
            <attr name = "pivotY" />
            <attr name = "drawable" />
        </declare-styleable>
(2)属性使用:
        <animated-rotate
            xmlns:android = "http://schemas.android.com/apk/res/android" 
            android:drawable = "@drawable/图片ID" 
            android:pivotX = "50%" 
            android:pivotY = "50%" 
            android:framesCount = "12" 
            android:frameDuration = "100"/>

7. string:字符串

(1)属性定义:
        <declare-styleable name = "MapView">
            <attr name = "apiKey" format = "string" />
        </declare-styleable>
(2)属性使用:
        <com.google.android.maps.MapView
            android:layout_width = "fill_parent"
            android:layout_height = "fill_parent"
            android:apiKey = "0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g"/>

8. fraction:百分数

(1)属性定义:
        <declare-styleable name="RotateDrawable">
            <attr name = "visible" />
            <attr name = "fromDegrees" format = "float" />
            <attr name = "toDegrees" format = "float" />
            <attr name = "pivotX" format = "fraction" />
            <attr name = "pivotY" format = "fraction" />
            <attr name = "drawable" />
        </declare-styleable>
(2)属性使用:
        <rotate
            xmlns:android = "http://schemas.android.com/apk/res/android"
            android:interpolator = "@anim/动画ID"
            android:fromDegrees = "0"    android:toDegrees = "360"
            android:pivotX = "200%"        android:pivotY = "300%"
            android:duration = "5000"    android:repeatMode = "restart"
            android:repeatCount = "infinite"/>

9. enum:枚举值

(1)属性定义:
        <declare-styleable name="名称">
            <attr name="orientation">
                  <enum name="horizontal" value="0" />
                  <enum name="vertical" value="1" />
            </attr>           
        </declare-styleable>
(2)属性使用:
        <LinearLayout
            xmlns:android = "http://schemas.android.com/apk/res/android"
            android:orientation = "vertical"
            android:layout_width = "fill_parent"
            android:layout_height = "fill_parent"/>
        </LinearLayout>

10. 位或运算

(1)属性定义:
        <declare-styleable name="名称">
            <attr name="windowSoftInputMode">
                    <flag name = "stateUnspecified" value = "0" />
                    <flag name = "stateUnchanged" value = "1" />
                    <flag name = "stateHidden" value = "2" />
                    <flag name = "stateAlwaysHidden" value = "3" />
                    <flag name = "stateVisible" value = "4" />
                    <flag name = "stateAlwaysVisible" value = "5" />
                    <flag name = "adjustUnspecified" value = "0x00" />
                    <flag name = "adjustResize" value = "0x10" />
                    <flag name = "adjustPan" value = "0x20" />
                    <flag name = "adjustNothing" value = "0x30" />
             </attr>        
        </declare-styleable>
(2)属性使用:
       <activity
             android:name = ".StyleAndThemeActivity"
             android:label = "@string/app_name"
             android:windowSoftInputMode = "stateUnspecified | stateUnchanged | stateHidden">
             <intent-filter>
                  <action android:name = "android.intent.action.MAIN" />
                  <category android:name = "android.intent.category.LAUNCHER" />
             </intent-filter>
        </activity>

注意:属性定义时可以指定多种类型值,使用“|”进行隔离多种类型。

(1)属性定义:
        <declare-styleable name = "名称">
            <attr name = "background" format = "reference|color" />
        </declare-styleable>
(2)属性使用:
         <ImageView
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:background = "@drawable/图片ID|#00FF00"/>

1.4、自定义属性用法

在我们自定义的View中有需要自定义的属性,则需要在values下建立attrs.xml,在其中定义你的属性。

a)在res/values文件下定义一个attrs.xml文件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ToolBar">
        <attr name="buttonNum" format="integer"/>
        <attr name="itemBackground" format="reference|color"/>
    </declare-styleable>
</resources>

b)在布局xml中如下使用该属性:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:toolbar="http://schemas.android.com/apk/res/cn.zzm.toolbar"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <cn.zzm.toolbar.ToolBar
        android:id="@+id/gridview_toolbar"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="@drawable/control_bar"
        android:gravity="center"
        toolbar:buttonNum="5"
        toolbar:itemBackground="@drawable/control_bar_item_bg" />
</RelativeLayout>

2、两种获取方式

我们可以在自定义View的构造函数中获取属性的值,有以下两种方式:

第一种获取方式:

TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.ToolBar);
int buttonNum = array.getInt(R.styleable.ToolBar_buttonNum, 5);
int itemBg = array.getResourceId(R.styleable.ToolBar_itemBackground, -1);
array.recycle();

第二种获取方式:

TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.ToolBar);
int count = array.getIndexCount();
for (int i = 0; i < count; i++) {
    int attr = array.getIndex(i);
    switch (attr) {
    case R.styleable.ToolBar_buttonNum:
        int buttonNum = array.getInt(attr, 5);
        break;
    case R.styleable.ToolBar_itemBackground:
        int itemBg = array.getResourceId(attr, -1);
        break;
    }
}

3、使用要点

属性类型:string、integer、dimension、reference、color、enum。

下面我看下使用自定义属性需要注意的一些地方,首先看下attrs.xml文件前面几种声明方式都是一致的,例如:

<attr name="buttonNum" format="integer"/>

只有enum是不同的,用法举例:

<attr name="testEnum">
<enum name="fill_parent" value="-1"/>
<enum name="wrap_content" value="-2"/>
</attr>

如果该属性可同时传两种不同的属性,则可以用“|”分割开即可。让我们再看看布局xml中需要注意的事项,在自定义组件的构造函数中使用:

TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.ToolBar);

获得对属性集的引用,然后就可以用“array”的各种方法来获取相应的属性值。这里需要注意的是,如果使用的方法和获取值的类型不对的话,则会返回默认值。

因此,如果一个属性是带两个及以上不用类型的属性,需要做多次判断,直到读取完毕后才能判断应该赋予何值。

1.5、自定义View演示

编写简单验证码程序:

a) 自定义View的属性,首先在res/value/下建立attrs.xml,里面定义我们的属性和声明我们的整个样式

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="titleText" format="string" />
    <attr name="titleTextColor" format="color" />
    <attr name="titleTextSize" format="dimension" />
    <declare-styleable name="CustomTitleView">
        <attr name="titleText" />
        <attr name="titleTextColor" />
        <attr name="titleTextSize" />
    </declare-styleable>
</resources>

b) 我们定义字体、字体颜色、字体大小等3个属性,foemat是指该属性的取值类型。

然后在布局文件中声明我们自定义的View,注意:自定义属性需要引入名称空间

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res/cn.legend.demo"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="cn.legend.demo.MainActivity" >
    
    <cn.legend.demo.CustomTitleView 
        android:layout_width="200dp"
        android:layout_height="100dp"
        custom:titleText="3721"
        custom:titleTextColor = "#ff0000"
        custom:titleTextSize="40sp"/>
</RelativeLayout>

c) 由于自定义属性,我们的View必须实现带attr的构造方法:

public class CustomTitleView extends View {
    private String mCustomText;
    private int mCustomTextColor;
    private int mCustomTextSize;
    private Rect mRect;
    private Paint mPaint;
    public CustomTitleView(Context context) {
        this(context, null);
    }
    public CustomTitleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomTitleView);
        mCustomText = array.getString(R.styleable.CustomTitleView_titleText);
        mCustomTextColor = array.getColor(R.styleable.CustomTitleView_titleTextColor, Color.BLACK);
        mCustomTextSize = (int) array.getDimension(R.styleable.CustomTitleView_titleTextSize, 16);
        array.recycle();
        
        initPaint();
    }
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setTextSize(mCustomTextSize);
        mPaint.setColor(mCustomTextColor);
        
        // 文字的骨骼
        mRect = new Rect();
        mPaint.getTextBounds(mCustomText, 0, mCustomText.length(), mRect);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 画背景
        mPaint.setColor(Color.YELLOW);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        
        mPaint.setColor(mCustomTextColor);
        canvas.drawText(mCustomText, getWidth() / 2 - mRect.width() /2, getHeight() / 2 + mRect.height() / 2, mPaint);
    }
}

运行效果:

8dfa9e64-a3e1-4b8d-8444-c5e46573a88a

d) 当我们将布局文件中的宽和高为明确的值时,系统测量的结果就是我们设置的结果,如果wrap_content或match_parent时,

系统默认会让宽和高和窗体同宽高,因为系统根本不知道我们的控件的具体大小,我们可以通过重写onMeasure方法

重写之前先了解MeasureSpec的specMode有三种类型:

  • EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
  • AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
  • UNSPECIFIED:表示子布局想要多大就多大,很少使用

e) 测量是比较复杂的,我们来测量下该控件:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
    int width,height;
    
    if(widthMode == MeasureSpec.EXACTLY){// 明确的值或match_parent
        width = widthSize;
    }else{
        mPaint.setTextSize(mCustomTextSize);
        mPaint.getTextBounds(mCustomText, 0, mCustomText.length(), mRect);
        int textWidth = mRect.width();
        // 期望的值,左右边距加上文本的宽度
        width = getPaddingLeft() + textWidth + getPaddingRight();
    }
    
    if(heightMode == MeasureSpec.EXACTLY){
        height = heightSize;
    }else{
        mPaint.setTextSize(mCustomTextSize);
        mPaint.getTextBounds(mCustomText, 0, mCustomText.length(), mRect);
        int textHeight = mRect.height();
        // 期望的值,上下边距加上文本的高度
        height = getPaddingTop() + textHeight + getPaddingBottom();
    }
    setMeasuredDimension(width, height);
}

由于我们让宽度和高度分别是左边距 + 右边距 + 文本宽度 和 上边距 + 下边距 + 文本高度,所以我们布局必须加入padding值:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res/cn.legend.demo"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="cn.legend.demo.MainActivity" >
    
    <cn.legend.demo.CustomTitleView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        custom:titleText="3721"
        android:padding="10dp"
        custom:titleTextColor = "#ff0000"
        custom:titleTextSize="40sp"/>
</RelativeLayout>

4a153301-37d8-4e10-96e2-a5091e0f0f2b

e) 接下来给该控件加入事件

this.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // 生成四位随机数
        Random random = new Random();
        Set<Integer> set = new HashSet<Integer>();
        while(set.size() < 4){
            int randomInt = random.nextInt(10);
            set.add(randomInt);
        }
        StringBuffer sb = new StringBuffer();
        for (Integer integer : set) {
            sb.append("" + integer);
        }
        mCustomText = sb.toString();
        // 重绘
        postInvalidate();
    }
});

##############################################待续

转载于:https://www.cnblogs.com/pengjingya/p/5510217.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值