第三章 Android控件架构(二)——自定义控件

Android给我们提供了丰富的组件库来创建丰富的UI效果,同时也提供了非常方便的扩展方法。通过继承Android的系统组件,我们可以非常方便地拓展现有功能,在系统组件的基础上创建新的功能,甚至可以直接自定义一个控件,实现Android系统控件所没有的功能。

一、简介

了解Android系统自定义View的过程,可以帮助我们了解系统的绘图机制。同时,在适当的情况下也可以通过自定义View来帮我们创建更加灵活的布局。

在自定义View时,我们通?;厝ブ匦磑nDraw()方法来绘制View的显示内容。如果该View还需要使用wrap_content属性,那么还需要重写onMeasure()方法。另外,通过自定义attrs属性,还可以设置新的属性配置值。

在View中通常有以下比较重要的回调方法。

  • onFinishInflate():从XML加载组件后回调。
  • onSizeChanged():组件大小改变时回调。
  • onMeasure():回调该方法来进行测量。
  • onLayout():回调该方法来确定显示的位置。
  • onTouchEvent():监听到触摸事件时回调。

当然,自定义View时,并不需要重写所有的方法,只需要重写特定条件的回调方法即可。这也是Android控件灵活性的表现。通常,有如下三种方法来实现自定义的控件。

  • 对现有控件进行拓展
  • 通过组合来实现新的控件
  • 重写View来实现全新的控件

二、对现有控件进行拓展

在原生控件的基础上进行拓展,是一种非常重要的自定义View方法,适用于增加新的功能、修改显示的UI等。


图3.1 闪动的文字效果

如图3.1所示,我们通过继承TextView控件,可以实现动态的文字闪动效果。CustomTextView类源码如下。

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;

/**
 * Created by caleb on 3/21/021.
 */

public class CustomTextView extends android.support.v7.widget.AppCompatTextView {

    private int mViewWidth;
    private int mTranslate;
    private Paint mPaint;
    private LinearGradient mLinearGradient;
    private Matrix mGradientMatrix;

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mGradientMatrix != null) {
            mTranslate += mViewWidth / 14;
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(mGradientMatrix);
            postInvalidateDelayed(100);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                mPaint = getPaint();
                mLinearGradient = new LinearGradient(
                        0, 0,   // 颜色渐变的起点
                        mViewWidth, 0,  // 颜色渐变的终点
                        new int[]{
                                Color.BLUE, 0xffffffff,
                                Color.BLUE},    // 渐变的颜色
                        null,   // 坐标,可以为空,值为0-1之间的float;为空表示颜色沿梯度线均匀分布
                        Shader.TileMode.CLAMP); // 平铺方式:CLAMP,平铺;MIRROR,镜像;REPEAT,重复
                mPaint.setShader(mLinearGradient);
                mGradientMatrix = new Matrix();
            }
        }
    }
}

然后,只需将布局文件中的TextView标签改成CustomTextView标签即可(完整类名)。

本示例中,利用了Android中Paint对象的Shader渲染器,通过设置一个不断变化的LinearGradient,并使用带有该属性的Paint对象来绘制要显示的文字。首先,在onSizeChanged()方法中进行一些对象的初始化工作,并根据View的宽度设置一个LinearGradient渐变渲染器。

然后,最关键的就是使用getPaint()方法获取当前绘制TextView的Paint对象,并给这个Paint对象设置原生TextView没有的LinearGradient属性。最后,在onDraw()方法中,通过矩阵的方式来不断平移渐变效果,从而在绘制文字时,产生动态的闪动效果。

综上,对现有组件进行拓展的关键步骤有两个:第一步,继承现有组件类,重写父类的某些方法,实现自定义组件;第二步在布局文件中使用自定义组件。

三、创建复合组件

创建复合控件可以很好地创建出具有重用功能的控件集合。这种方式通常是继承一个合适的ViewGroup,然后在给它添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们可以给它指定一些可配置的属性,让它具有更强的拓展性。

下面给出一个自定义TopBar的示例。

首先,我们需要定义复合组件的属性,在res\values中创建一个attrs.xml的属性定义文件,代码如下。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--声明使用自定义属性-->
    <declare-styleable name="CustomTopBar">
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <attr name="titleBackground" format="reference|color" />
        <attr name="leftColor" format="color" />
        <attr name="leftBackground" format="reference|color" />
        <attr name="leftText" format="string" />
        <attr name="rightColor" format="color" />
        <attr name="rightBackground" format="reference|color" />
        <attr name="rightText" format="string" />
    </declare-styleable>
</resources>

通过<declare-styleable>标签声明使用自定义属性,通过name来确定引用的名称。最后,使用<attr>标签来声明具体的自定义属性,并使用format来指定属性的类型。特别地,有一些属性可以是颜色,也可以是引用属性,使用“|”来分隔,如reference|color。

确定好属性后,就可以创建一个自定义组件——CustomTopBar,并让它继承自ViewGroup,比如RelativeLayout,代码如下。

package com.example.caleb.customviews;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;

/**
 * Created by caleb on 3/23/023.
 */

public class CustomTopBar extends RelativeLayout {

    private String mTitle;
    private String mLeftText;
    private String mRightText;
    private int mTitleColor;
    private int mLeftColor;
    private int mRightColor;
    private float mTitleSize;

    private Drawable mTitleBackground;
    private Drawable mLeftBackground;
    private Drawable mRightBackground;

    private TextView mTitleView;
    private Button mLeftButton;
    private Button mRightButton;

    private LayoutParams mTitleParams;
    private LayoutParams mLeftParams;
    private LayoutParams mRightParams;

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public CustomTopBar(Context context, AttributeSet attrs) {
        super(context, attrs);

        // 取出xml文件中自定义的属性值,并存储到TypedArray中
        @SuppressLint("Recycle") TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomTopBar);
        mTitle = ta.getString(R.styleable.CustomTopBar_title);
        mLeftText = ta.getString(R.styleable.CustomTopBar_leftText);
        mRightText = ta.getString(R.styleable.CustomTopBar_rightText);
        mTitleColor = ta.getColor(R.styleable.CustomTopBar_titleTextColor, 0);
        mLeftColor = ta.getColor(R.styleable.CustomTopBar_leftColor, 0);
        mRightColor = ta.getColor(R.styleable.CustomTopBar_rightColor, 0);
        mTitleSize = ta.getDimension(R.styleable.CustomTopBar_titleTextSize, 10);
        mTitleBackground = ta.getDrawable(R.styleable.CustomTopBar_titleBackground);
        mLeftBackground = ta.getDrawable(R.styleable.CustomTopBar_leftBackground);
        mRightBackground = ta.getDrawable(R.styleable.CustomTopBar_rightBackground);
        // 获取完属性值后,使用recycle()方法避免重新创建时的错误
        ta.recycle();

        // 组合组件
        mTitleView = new TextView(context);
        mLeftButton = new Button(context);
        mRightButton = new Button(context);

        mLeftButton.setText(mLeftText);
        mLeftButton.setTextColor(mLeftColor);
        mLeftButton.setBackground(mLeftBackground);

        mRightButton.setText(mRightText);
        mRightButton.setTextColor(mRightColor);
        mRightButton.setBackground(mRightBackground);

        mTitleView.setText(mTitle);
        mTitleView.setTextSize(mTitleSize);
        mTitleView.setTextColor(mTitleColor);
        mTitleView.setBackground(mTitleBackground);
        mTitleView.setGravity(Gravity.CENTER);

        // 为组件元素设置相应的布局元素
        mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
        mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);

        // 添加到ViewGroup
        addView(mTitleView, mTitleParams);
        addView(mLeftButton, mLeftParams);
        addView(mRightButton, mRightParams);
    }

    /**
     * 接口对象,实现回调机制
     */
    public interface OnTopBarClickListener {
        void onLeftClick();

        void onRightClick();
    }

    private OnTopBarClickListener mListener;

    public void setOnTopBarClickListener(final OnTopBarClickListener mListener) {
        this.mListener = mListener;
        if (mLeftButton != null) {
            mLeftButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    mListener.onLeftClick();
                }
            });
        }
        if (mRightButton != null) {
            mRightButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    mListener.onRightClick();
                }
            });
        }
    }

    /**
     * 设置按钮的显示与否。通过id区分按钮,flag区分是否显示。
     *
     * @param id   id
     * @param flag 是否显示
     */
    public void setButtonVisible(int id, boolean flag) {
        if (flag) {
            if (id == 0) {
                mLeftButton.setVisibility(View.VISIBLE);
            } else {
                mRightButton.setVisibility(View.VISIBLE);
            }
        } else {
            if (id == 0) {
                mLeftButton.setVisibility(View.GONE);
            } else {
                mRightButton.setVisibility(View.GONE);
            }
        }
    }
}

系统提供了TypedArray这样的数据结构来获取自定义属性集,后面引用的styleable的CustomTopBar,就是XML文件中定义的name名。

接下来,我们就可以开始组合控件了,具体见代码中的注释。添加完控件时,使用addView()方法将组合的三个控件加入到定义的TopBar模板中,并给他们设置我们前面所获取到的具体的属性值。

定义完组件之后,我们接下来演示一下如何使用。

使用自定义组合控件时,首先需要在布局中引用自定义组件。在布局文件中,有如下代码。

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

这行代码就是在指定引用的名字空间xmlns,即xml namespace。这里指定了名字空间“android”,因此接下来在使用系统属性的时候,才可以使用“android:”来引用Android的系统属性。

同样地,使用自定义属性的时候,也需要创建自己的名字空间,示例代码中使用了如下代码引入名字空间app。

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

之后,在XML文件中使用自定义的属性时,就可以通过这个名字空间来应用,完整代码如下。

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.caleb.customviews.MainActivity">

    <com.example.caleb.customviews.CustomTopBar
        android:id="@+id/topBar"
        android:layout_width="match_parent"
        android:layout_height="40dp"

        app:leftBackground="@drawable/back_bg"
        app:leftText="Back"
        app:leftColor="#FFFFFF"
        app:rightBackground="@drawable/more_bg"
        app:rightText="More"
        app:rightColor="#FFFFFF"
        app:title="自定义标题"
        app:titleTextColor="#123412"
        app:titleTextSize="10sp"
        app:titleBackground="@drawable/title_bg"
        />

</RelativeLayout>

最后,即是在Activity中进行相关的设置即可。代码如下。

package com.example.caleb.customviews;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";
    private CustomTopBar mTopBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTopBar = (CustomTopBar) findViewById(R.id.topBar);
        mTopBar.setOnTopBarClickListener(new CustomTopBar.OnTopBarClickListener() {
            @Override
            public void onLeftClick() {
                Toast.makeText(mTopBar.getContext(), "你点击了左边的按钮", Toast.LENGTH_SHORT).show();
                Log.i(TAG, "onLeftClick: ");
            }

            @Override
            public void onRightClick() {
                Toast.makeText(mTopBar.getContext(), "你点击了右边的按钮", Toast.LENGTH_SHORT).show();
                Log.i(TAG, "onRightClick: ");
            }
        });
    }
}

代码执行效果如图3.2所示。


3.2 复合组件示例效果

总结一下,创建复合组件的步骤为:

  1. 定义属性。创建属性文件,定义属性名以及类型等。
  2. 组合控件。继承ViewGroup,组合所需的控件,完成复合控件的定义。
  3. 引用。在布局文件中引用复合组件,使用自定义名字空间来使用自定义组建的属性。
  4. 在Activity中使用自定义复合组件,达到UI渲染显示的效果。

四、重写View来实现全新的控件

当Android系统原生的控件无法满足我们的需求时,我们就可以完全创建一个新的自定义View来实现需要的功能。

重写View首先需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。当然,我们还可以像实现组合控件方式那样,通过引入自定义属性,丰富自定义View的可定制性。

具体步骤和前面差不多,主要还是自定义组件类、布局文件、自定义属性文件等,在此就不再举例具体说明了。

五、总结

最后,总结一下自定义控件的一些重要部分和步骤。

总的来说,自定义控件,首先需要有一个自定义的控件类,它继承了现有的控件或者View类或者View的子类,实现了自定义控件的一些属性和功能。

其次,对于一些需要自定义属性的控件,需要定义一个属性文件,它表明了自定义控件的引用的名字,各属性的名字及类型等信息。

最后,便是在布局文件和Activity中使用自定义控件。

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,941评论 25 707
  • 6、View的绘制 (1)当测量好一个View之后,我们就可以简单的重写 onDraw()方法,并在 Canvas...
    b5e7a6386c84阅读 1,890评论 0 3
  • 【1】 前段时间翻了翻2017年度目标,有一项是今年需要写24篇文;我再看了眼简书文章记录,隐约觉得这会是个脱离实...
    tonyhi阅读 377评论 0 0
  • 转身,我就在你身后 1 这是一个流传已广的小故事。 “她迷路了,给三个人发了微信,第一位说,注意安全!第二位说,需...
    生达成长规划阅读 1,136评论 0 1
  • 你的生命是为何而生?反正我是为了情!而且还是情关! 这段时间与老公老是有磨擦,搞到都要内心崩溃了,整个人无激情,行...
    jessica258130阅读 334评论 2 0