自定义View初步

自定义View的过程
1.自定义View的属性
2.在构造方法中获得我们的属性
3.[重写onMeasure方法]
我把3用[]标出了 所以说3不一定是必须的,当然了大部分情况下还是需要重写的。
4.重写onDraw方法

1.自定义view的属性,在value/attrs目录下,在里面定义我们的属性和样式:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="myText">
        <attr name="title_Text" format="string"/>
        <attr name="title_text_Color" format="color"/>
        <attr name="title_Text_Size" format="dimension"/>
    </declare-styleable>
</resources>

我们定义了字体,字体颜色,字体大小3个属性,format是值该属性的取值类型:
一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一下吧。

我们可以在我们的布局引入我们自定义的控件,
注意,平时,我们直接使用SDK的系统自带的控件,一般位于android.widget这个包路径下,在xml文件中,系统也会跟着这个路径找到我们的控件但是对于我们自定义的控件,我们需要填入控件的包路径,比如com.mytext.myview.MyView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.mytext.myview.MyView
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:title_Text=zhihao的练习的View"
        app:title_Text_Color="#ff0000"
        app:title_Text_Size="18sp"/>
</RelativeLayout>

2.在构造方法,获得我们自定义样式,

我们先了解下几个重要的东西:

obtainStyledAttributes函数获取属性
其实我们在前面已经使用了obtainStyledAttributes来获取属性了,现在来看看这个函数的声明吧:

obtainAttributes(AttributeSet set, int[] attrs) //从layout设置的属性集中获取attrs中的属性
obtainStyledAttributes(int[] attrs) //从系统主题中获取attrs中的属性
obtainStyledAttributes(int resId,int[] attrs) //从资源文件定义的style中读取属性
obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)//这是最复杂的一种情况,后面细说。

==这么多重载的方法是不是已经看懵了?其实你只需要理解其中的参数就能掌握各个方法的使用方法。所谓获取属性,无非就是需要两个参数:第一,我需要获取那些属性;第二:我从哪里去获取这些属性(数据源)。==

attrs
attrs:int[],每个方法中都有的参数,就是告诉系统需要获取那些属性的值。

set
set:表示从layout文件中直接为这个View添加的属性的集合,如:android:layout_width=”match_parent”。注意,这里面的属性必然是通过xml配置添加的,也就是由LayoutInflater加载进来的布局或者View`才有这个属性集。

现在你知道为啥我们在自己定义View的时候至少要重写(Context context, AttributeSet set)构造器了吧?因为不重写时,我们将无法获取到layout中配置的属性!!当然,也因为这样,LayoutInflater在inflater布局时会通过反射去调用View的(Context context, AttributeSet attrs)构造器。
set 中实际上又有两种数据来源,当然最后都会包含在set中。一种是直接使用android:layout_width=”wrap_content”这种直接指定的,还有一种是通过style=”@style/somestyle”这样指定的。
defStyleAttr
这个参数是本文的关键所在,也是自定义一个可以在Theme中配置的样式的关键,先看个栗子吧:
如果我想通过在系统主题里面设置一个样式,修改所有textview的样式,你一般会这么做:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    //在主题中设置textviewstyle
    <item name="android:textViewStyle">@style/textviewstyle</item>
</style>

<style name="textviewstyle" parent="android:style/Widget.TextView">
    <!--指定一些属性-->
</style>

首先android:textViewStyle其实就是一个普通的在资源文件中定义的属性attr,它的format=”reference”。那问题来了,TextView是怎么得知我们自己定义的textviewstyle的呢?这其实就是defStyleAttr的应用场景:定义Theme可配置样式。

public TextView(Context context, AttributeSet attrs) {
    //指定属性textViewStyle为defStyleAttr,然后系统会去搜索Theme中你为这个
    //属性配置的style
    this(context, attrs, com.android.internal.R.attr.textViewStyle);
  }

  public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
    //最终调用到View的第四个构造器时,调用了obtainStyledAttributes
    TypedArray a = theme.obtainStyledAttributes(attrs,
            com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
  }

resId=defStyleRes
resId or defStyleRes:直接从资源文件中定义的某个样式中读取。

NULL
看看第二个方法吧,里面除了指定了attrs属性集之外没有任何属性值来源,数据从哪儿来呢?原来我们可以直接在Theme中指定属性的值,那么NULL表示直接从Theme中读取属性。

是不是看到这里你已经有点迷糊了?不要紧,耐心看下去,后面有一个例子,看完例子你再回头看看这里的说明就ok了。

四个参数的obtainStyledAttributes
看看这个方法,返回的结果还是我们所关心的attrs(int[])中包含的属性集。那么数据来源呢?一共有4个,set,defStyleAttr,NULL,defStyleRes,如果一个属性在多个地方都被定义了,那么以哪个为准?

优先级如下
set>defStyleAttr(主题可配置样式)>defStyleRes(默认样式)>NULL(主题中直接指定)

TypedArray
我们看到在获取到属性值之后,都会返回一个TypedArray对象,它又是什么鬼?TypedArray主要有两个作用,第一是内部去转换attrid和属性值数组的关系;第二是提供了一些类型的自动转化,比如我们getString时,如果你是通过@string/hello这种方式设置的,TypedArray会自动去将ResId对应的string从资源文件中读出来。说到底,都是为了方便我们获取属性参数。
回到主题: 现在我们应该知道如何为我们的自定义View添加在主题中可配置的Style,主要是通obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)方法来做,需要注意的是,defStyleAttr和defStyleRes都可以设置成0表示不去搜索可配置的风格和默认风格。

如下:

package com.mytext.myview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

/**
 * @author Created by zhihao on 2016/10/14.
 * @describe
 * @version_
 **/
public class MyView extends View {

    /**
     * 文本
     */
    private String mTitleText;
    /**
     * 文本的颜色
     */
    private int mTitleTextColor;
    /**
     * 文本的大小
     */
    private int mTitleTextSize;

    /**
     * 绘制时控制文本绘制的范围
     */
    private Rect mBound;
    private Paint mPaint;

    public MyView(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

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

    /**
     * 获得我自定义的样式属性
     *
     * @param context
     * @param attrs
     * @param defStyle
     */
    public MyView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
        /**
         * 获得我们所定义的自定义样式属性
         */
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.myText, defStyle, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++)
        {
            int attr = a.getIndex(i);
            switch (attr)
            {
                case R.styleable.myText_title_Text:
                    mTitleText = a.getString(attr);
                    break;
                case R.styleable.myText_title_Text_Color:
                    // 默认颜色设置为黑色
                    mTitleTextColor = a.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.myText_title_Text_Size:
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, this.getResources().getDisplayMetrics()));
                    break;

            }

        }
        a.recycle();

        /**
         * 获得绘制文本的宽和高
         */
        mPaint = new Paint();
        mPaint.setTextSize(mTitleTextSize);
        // mPaint.setColor(mTitleTextColor);
        mBound = new Rect();
        mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);

    }
}
3.重写onDraw方法,利用系统提供的onMeasure方法
@Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
    {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
    }  

    @Override  
    protected void onDraw(Canvas canvas)  
    {  
        mPaint.setColor(Color.YELLOW);  
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);  

        mPaint.setColor(mTitleTextColor);  
        canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);  
    }  

效果如下:
这里写图片描述
此处,需要注意的是,当我们设置wrap_content时,如果不重写onMeasure方法,效果会跟match_parent一样。结果如下:
这里写图片描述
此时,得到的结果跟我们所需要的截然不同。
why???
系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。
所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:
重写之前先了解MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
下面贴出重写onMeasure方法的代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int hegihtSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (widthSpecMode == MeasureSpec.EXACTLY) {//MATH

            width = widthSpecSize;
        } else {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
            float textWidth = mBound.width();
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            width = desired;
        }
        if (hegihtSpecMode == MeasureSpec.EXACTLY) {//MATH

            height = heightSpecSize;
        } else {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
            float textWidth = mBound.width();
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            height = desired;
        }
        setMeasuredDimension(width, height);
    }

最后效果如下:
这里写图片描述

完全复合我们的预期,现在我们可以对高度、宽度进行随便的设置了,基本可以满足我们的需求。
当然了,这样下来我们这个自定义View与TextView相比岂不是没什么优势,所有我们觉得给自定义View添加一个事件:
在构造中添加:


    public interface MyonClickLister {

        public void MyOnclick(View v);

    }

    public void setMyonClickLister(MyonClickLister lister) {

        this.myonClickLister = lister;
    }

     this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {

                myonClickLister.MyOnclick(v);
            }
        });

    }
    //actvity运用如下:
         myView.setMyonClickLister(new MyView.MyonClickLister() {
            @Override
            public void MyOnclick(View v) {
                Toast.makeText(getApplicationContext(), "被点击了", Toast.LENGTH_LONG).show();
            }
        });

这里写图片描述
最后的最后,需要说几个需要注意的点
1.现在一般都用android studio开发了,使用自定义View,必须在xml文件头部引入:一定要引入 xmlns:app=”http://schemas.android.com/apk/res-auto”
2.之前我设置text的颜色引用的是titleTextColor,我运行的时候回报错:”titleTextSize has been defined…”,证明titleTextColor被其他控件运用过,无奈之下改为title_text_Color。。这里为了以后防止出现引用相同的属性名,可以声明属性,重复使用,只不会重复声明属性了,如下:


    <attr name="titleText" format="string"/>
    <attr name="title_Text_color" format="color"/>
    <attr name="title_Text_Size" format="dimension"/>
3.对于重写onMeasure方法,可以查看我写的另外的文章:  http://blog.csdn.net/zhuangxiaozhi/article/details/52793567

好了,View的初步教程到这里了,github地址为:https://github.com/zhuangzhitu/MyView 欢迎stars,谢谢了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值