android 自定义 View(2)

来源:互联网 发布:c语言头文件是什么意思 编辑:程序博客网 时间:2024/06/03 23:02

参考:

android 自定义 View - 参考


自定义视图(View)是 Android 开发的一个进阶内容。随着开发的深入,肯定会出现系统提供的基础控件不符合需求的情况。一方面通过组合基础控件以形成新的布局,另一方面可以通过自定义控件的方式来更加灵活的实现需求

自定义视图涉及到 Android 系统许多方面的内容,下面根据自己的理解顺序来讲一讲如何自定义视图


主要内容

  1. 视图创建
  2. 自定义属性
  3. 完整代码

视图创建

可以通过代码(from code)或者布局文件(from XML layout file)增加视图,根据不同的载入方式,将调用不同的构造函数

构造函数

View 类提供了 4 种构造函数,其分别有 1-4 个参数:

public MyView(Context context) {    super(context);}public MyView(Context context, AttributeSet attrs) {    super(context, attrs);}public MyView(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);}public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {    super(context, attrs, defStyleAttr, defStyleRes);}
  • 单个参数:

    /** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can *        access the current theme, resources, etc. */public View(Context context) {    ...}

    当视图在代码中创建时,调用此构造函数

  • 两个参数:

    /** * Constructor that is called when inflating a view from XML. This is called * when a view is being constructed from an XML file, supplying attributes * that were specified in the XML file. This version uses a default style of * 0, so the only attribute values applied are those in the Context's Theme * and the given AttributeSet. * * <p> * The method onFinishInflate() will be called after all children have been * added. * * @param context The Context the view is running in, through which it can *        access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @see #View(Context, AttributeSet, int) */public View(Context context, @Nullable AttributeSet attrs) {    this(context, attrs, 0);}

    当视图在 xml 布局文件中定义时,调用此构造函数

  • 三个参数:

    /** * Perform inflation from XML and apply a class-specific base style from a * theme attribute. This constructor of View allows subclasses to use their * own base style when they are inflating. For example, a Button class's * constructor would call this version of the super class constructor and * supply <code>R.attr.buttonStyle</code> for <var>defStyleAttr</var>; this * allows the theme's button style to modify all of the base view attributes * (in particular its background) as well as the Button class's attributes. * * @param context The Context the view is running in, through which it can *        access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyleAttr An attribute in the current theme that contains a *        reference to a style resource that supplies default values for *        the view. Can be 0 to not look for defaults. * @see #View(Context, AttributeSet) */public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {    this(context, attrs, defStyleAttr, 0);}

    该构造函数同样表示当自定义视图在布局文件中定义时使用,不过它和上一个构造函数的区别是多了一个 defStyleAttr 属性。当自定义视图继承自 View 子类,比如按钮(button),调用此构造函数,属性 defStyleAttr 就表示按钮的基本样式属性

  • 四个参数:

    /** * Perform inflation from XML and apply a class-specific base style from a * theme attribute or style resource. This constructor of View allows * subclasses to use their own base style when they are inflating. * <p> * When determining the final value of a particular attribute, there are * four inputs that come into play: * <ol> * <li>Any attribute values in the given AttributeSet. * <li>The style resource specified in the AttributeSet (named "style"). * <li>The default style specified by <var>defStyleAttr</var>. * <li>The default style specified by <var>defStyleRes</var>. * <li>The base values in this theme. * </ol> * <p> * Each of these inputs is considered in-order, with the first listed taking * precedence over the following ones. In other words, if in the * AttributeSet you have supplied <code>&lt;Button * textColor="#ff000000"&gt;</code> * , then the button's text will <em>always</em> be black, regardless of * what is specified in any of the styles. * * @param context The Context the view is running in, through which it can *        access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyleAttr An attribute in the current theme that contains a *        reference to a style resource that supplies default values for *        the view. Can be 0 to not look for defaults. * @param defStyleRes A resource identifier of a style resource that *        supplies default values for the view, used only if *        defStyleAttr is 0 or can not be found in the theme. Can be 0 *        to not look for defaults. * @see #View(Context, AttributeSet, int) */public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {    ...}

    最后一个构造函数是从 Android 5.0(API 21) 开始加入的,如果 minSdkVersion 小于 21 的话,自定义视图不需要重载它

通常的使用方式是在自定义视图中重载前 3 个构造函数:

public class MyView extends View {    public MyView(Context context) {//        super(context);        this(context, null);    }    public MyView(Context context, @Nullable AttributeSet attrs) {//        super(context, attrs);        this(context, attrs, 0);    }    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }}

构造结束判断

当自定义视图从布局文件中载入结束后,会调用方法 onFinishInflate

/** * Finalize inflating a view from XML.  This is called as the last phase * of inflation, after all child views have been added. * * <p>Even if the subclass overrides onFinishInflate, they should always be * sure to call the super method, so that we get called. */@CallSuperprotected void onFinishInflate() {}

Note:需要调用超类方法(super method

测试代码如下:

public class MyView extends View {    private static final String TAG = "MyView";    public MyView(Context context) {//        super(context);        this(context, null);        Log.e(TAG, "MyView: one");    }    public MyView(Context context, @Nullable AttributeSet attrs) {//        super(context, attrs);        this(context, attrs, 0);        Log.e(TAG, "MyView: two");    }    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        Log.e(TAG, "MyView: three");    }    @Override    protected void onFinishInflate() {        super.onFinishInflate();        Log.e(TAG, "onFinishInflate: ");    }}

布局文件定义:

<?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"    tools:context="com.zj.viewtest.MainActivity">    <com.zj.viewtest.MyView        android:layout_width="200dp"        android:layout_height="200dp"        android:background="@android:color/black" /></LinearLayout>

运行,日志如下:

这里写图片描述

由图可知,自定义视图调用顺序如下:

  • public MyView(Context context, @Nullable AttributeSet attrs)
  • public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
  • protected void onFinishInflate()

自定义属性

当自定义视图在布局文件中定义时,可以根据需要自定义属性

自定义属性分为 4 个基本步骤:

  • 使用 <declare-styleable> 资源元素定义自定义属性(Define custom attributes for your view in a <declare-styleable> resource element
  • 在布局文件中使用自定义属性(Specify values for the attributes in your XML layout
  • 从构造函数中解析自定义属性(Retrieve attribute values at runtime
  • 在绘图中使用自定义属性(Apply the retrieved attribute values to your view

定义

自定义属性使用资源元素(resource element<declare-styleable>,通常将其放置在 res/values/attrs.xml 文件中(工程如果没有此资源文件,新建一个):

元素 <declare-styleable> 作为 <resource> 元素的子元素,包含属性 name,指明此自定义属性集:

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="CustomView" ></declare-styleable></resources>

Note:通常将属性 name 赋值为使用的自定义视图名

在元素 <declare-styleable> 下声明子元素 <attr>,每个子元素表示一个自定义属性

元素 <attr> 需要声明两个属性:nameformat

属性 name 表示该属性名称,属性 format 表示该属性格式,共有 10 种格式:

  • enum
  • boolean
  • color
  • dimension
  • flag
  • float
  • fraction
  • integer
  • reference
  • string

定义 3 个自定义属性,分别表示文本内容,文本大小和文本颜色,示例如下:

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="MyView">        <attr name="text" format="string" />        <attr name="textSize" format="dimension" />        <attr name="textColor" format="color" />    </declare-styleable></resources>

也可以在根元素 <resource> 下先定义好 <attr> 元素,然后在元素 <declare-stylebale> 中使用:

<?xml version="1.0" encoding="utf-8"?><resources>    <attr name="text" format="string" />    <attr name="textSize" format="dimension" />    <attr name="textColor" format="color" />    <declare-styleable name="MyView">        <attr name="text" />        <attr name="textSize" />        <attr name="textColor" />    </declare-styleable></resources>

赋值

定义完成后,就可以在布局文件的自定义组件中使用这些属性

首先需要在根元素中定义自定义属性的命名空间(namespace),使用指令 xmlns:name=value,其中 name 为命名空间名称(自定义),value 为命名空间,值为 http://schemas.android.com/apk/res/[your package name],比如:

xmlns:custom="http://schemas.android.com/apk/com.zj.viewtest"

也可使用系统默认的

xmlns:app="http://schemas.android.com/apk/res-auto"

在布局文件中使用自定义视图时,需要完全限定名(the fully qualified name),即完整的名称,比如 com.example.MyView;如果自定义视图是内部类,也需要完整名称,比如文件 MyView.java 中的内部类 InnerView,其名为 com.example.MyView&InnerView

自定义属性的使用方式和原生属性无异,唯一有的区别可能就是没有自动提示了:

<?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:custom="http://schemas.android.com/apk/res/com.zj.viewtest"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    tools:context="com.zj.viewtest.MainActivity">    <com.zj.viewtest.MyView        android:layout_width="200dp"        android:layout_height="200dp"        android:background="@color/colorAccent"        custom:text="Hello World"        custom:textColor="@android:color/holo_blue_bright"        custom:textSize="16sp" /></LinearLayout>

获取

当自定义视图在布局文件中定义时,构造对象时使用的构造函数为:

public View(Context context, @Nullable AttributeSet attrs)

布局文件中使用的属性均放置在参数 attrs 中,Android 推荐不直接从 AttributeSet 对象中取出属性值,它列出两点原因:

  • Resource references within attribute values are not resolved
  • Styles are not applied

将参数 attrs 输入函数 obtainStyledAttributes,然后从这里获取属性值

public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    Log.e(TAG, "MyView: three");    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);    try {        text = a.getString(R.styleable.MyView_text);        textSize = a.getDimension(R.styleable.MyView_textSize, (int) TypedValue.applyDimension(                TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));        textColor = a.getColor(R.styleable.MyView_textColor, Color.BLACK);    } finally {        a.recycle();    }    Log.e(TAG, "MyView: text = " + text);    Log.e(TAG, "MyView: textSize = " + textSize);    Log.e(TAG, "MyView: textColor = " + textColor);}

Note 1:属性值的写法 - R.styleable.自定义属性_属性名,比如 R.styleable.MyView_text

Note 2: 在检索完属性值后,需要将 TypedArray 回收


Android 自定义View (一)
中看到另一种检索属性值的方式,参考一下:

public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    Log.e(TAG, "MyView: three");    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);    try {        int count = a.getIndexCount();        for (int i = 0; i < count; i++) {            int attr = a.getIndex(i);            switch (attr) {                case R.styleable.MyView_text:                    text = a.getString(attr);                    break;                case R.styleable.MyView_textSize:                    textSize = a.getDimension(attr, (int) TypedValue.applyDimension(                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));                    break;                case R.styleable.MyView_textColor:                    textColor = a.getColor(attr, Color.BLACK);                    break;            }        }    } finally {        a.recycle();    }    Log.e(TAG, "MyView: text = " + text);    Log.e(TAG, "MyView: textSize = " + textSize);    Log.e(TAG, "MyView: textColor = " + textColor);}

获取的属性值如下:

这里写图片描述

使用

获取自定义的属性值后,就可以在绘图操作中使用

绘图操作最重要的部分就是重载 onDraw 方法

/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */protected void onDraw(Canvas canvas) {}

参数 canvas 是画布,另外还需要定义画笔 Paint

实现如下:

public class MyView extends View {    private static final String TAG = "MyView";    private String text;    private float textSize;    private int textColor;    private Paint paint;    private Rect textBound;    ...    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        Log.e(TAG, "MyView: three");        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);        try {            int count = a.getIndexCount();            for (int i = 0; i < count; i++) {                int attr = a.getIndex(i);                switch (attr) {                    case R.styleable.MyView_text:                        text = a.getString(attr);                        break;                    case R.styleable.MyView_textSize:                        textSize = a.getDimension(attr, (int) TypedValue.applyDimension(                                TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));                        break;                    case R.styleable.MyView_textColor:                        textColor = a.getColor(attr, Color.BLACK);                        break;                }            }        } finally {            a.recycle();        }        Log.e(TAG, "MyView: text = " + text);        Log.e(TAG, "MyView: textSize = " + textSize);        Log.e(TAG, "MyView: textColor = " + textColor);        paint = new Paint();        textBound = new Rect();    }    ...    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        int width = getWidth();        int height = getHeight();        paint.setColor(textColor);        paint.setTextSize(textSize);        paint.getTextBounds(text, 0, text.length(), textBound);        canvas.drawText(text, width / 2 - textBound.width() / 2, height / 2 + textBound.height() / 2, paint);    }}

结果:

这里写图片描述

属性封装

在应用运行过程中,可能会需要改变自定义视图的属性,可以通过 setter getter 方法,封装自定义属性

MyView.java 中增加如下代码:

public String getText() {    return text;}public void setText(String text) {    this.text = text;    invalidate();    requestLayout();}public float getTextSize() {    return textSize;}public void setTextSize(float textSize) {    this.textSize = textSize;    invalidate();    requestLayout();}public int getTextColor() {    return textColor;}public void setTextColor(int textColor) {    this.textColor = textColor;    invalidate();    requestLayout();}

Note:在 setter 方法中除了改变属性值外,还需要调用方法 invalidaterequestLayout,以确保通知系统重绘该视图


完整代码

attrs.xml

<?xml version="1.0" encoding="utf-8"?><resources>    <attr name="text" format="string" />    <attr name="textSize" format="dimension" />    <attr name="textColor" format="color" />    <declare-styleable name="MyView">        <attr name="text" />        <attr name="textSize" />        <attr name="textColor" />    </declare-styleable></resources>

MyView.java

public class MyView extends View {    private static final String TAG = "MyView";    private String text;    private float textSize;    private int textColor;    private Paint paint;    private Rect textBound;    public MyView(Context context) {//        super(context);        this(context, null);        Log.e(TAG, "MyView: one");    }    public MyView(Context context, @Nullable AttributeSet attrs) {//        super(context, attrs);        this(context, attrs, 0);        Log.e(TAG, "MyView: two");    }    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        Log.e(TAG, "MyView: three");        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);        try {            int count = a.getIndexCount();            for (int i = 0; i < count; i++) {                int attr = a.getIndex(i);                switch (attr) {                    case R.styleable.MyView_text:                        text = a.getString(attr);                        break;                    case R.styleable.MyView_textSize:                        textSize = a.getDimension(attr, (int) TypedValue.applyDimension(                                TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));                        break;                    case R.styleable.MyView_textColor:                        textColor = a.getColor(attr, Color.BLACK);                        break;                }            }        } finally {            a.recycle();        }        Log.e(TAG, "MyView: text = " + text);        Log.e(TAG, "MyView: textSize = " + textSize);        Log.e(TAG, "MyView: textColor = " + textColor);        paint = new Paint();        textBound = new Rect();    }    @Override    protected void onFinishInflate() {        super.onFinishInflate();        Log.e(TAG, "onFinishInflate: ");    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        int width = getWidth();        int height = getHeight();        paint.setColor(textColor);        paint.setTextSize(textSize);        paint.getTextBounds(text, 0, text.length(), textBound);        canvas.drawText(text, width / 2 - textBound.width() / 2, height / 2 + textBound.height() / 2, paint);    }    public String getText() {        return text;    }    public void setText(String text) {        this.text = text;        invalidate();        requestLayout();    }    public float getTextSize() {        return textSize;    }    public void setTextSize(float textSize) {        this.textSize = textSize;        invalidate();        requestLayout();    }    public int getTextColor() {        return textColor;    }    public void setTextColor(int textColor) {        this.textColor = textColor;        invalidate();        requestLayout();    }}

activity_main.xml

<?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:custom="http://schemas.android.com/apk/res/com.zj.viewtest"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    tools:context="com.zj.viewtest.MainActivity">    <com.zj.viewtest.MyView        android:id="@+id/my_view"        android:layout_width="200dp"        android:layout_height="200dp"        android:background="@color/colorAccent"        custom:text="Hello World"        custom:textColor="@android:color/holo_blue_bright"        custom:textSize="16sp" />    <Button        android:id="@+id/btn_text"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_marginTop="30dp"        android:text="TEXT" />    <Button        android:id="@+id/btn_color"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="COLOR" /></LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {    private MyView myView;    private Button btnText;    private Button btnColor;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        myView = (MyView) findViewById(R.id.my_view);        btnText = (Button) findViewById(R.id.btn_text);        btnColor = (Button) findViewById(R.id.btn_color);        final String[] randomText = getResources().getStringArray(android.R.array.phoneTypes);        btnText.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Random random = new Random();                myView.setText(randomText[random.nextInt(randomText.length)]);            }        });        final int[] randomColor = new int[]{Color.GREEN, Color.GRAY, Color.WHITE, Color.BLUE, Color.BLACK};        btnColor.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Random random = new Random();                myView.setTextColor(randomColor[random.nextInt(randomColor.length)]);            }        });    }}
原创粉丝点击