美文网首页
第三章 Android控件架构(二)——自定义控件

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

作者: Bejamin | 来源:发表于2018-03-21 22:29 被阅读15次

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

    一、简介

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

    在自定义View时,我们通常回去重写onDraw()方法来绘制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中使用自定义控件。

    相关文章

      网友评论

          本文标题:第三章 Android控件架构(二)——自定义控件

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