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所示,我们通过继承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所示。
总结一下,创建复合组件的步骤为:
- 定义属性。创建属性文件,定义属性名以及类型等。
- 组合控件。继承ViewGroup,组合所需的控件,完成复合控件的定义。
- 引用。在布局文件中引用复合组件,使用自定义名字空间来使用自定义组建的属性。
- 在Activity中使用自定义复合组件,达到UI渲染显示的效果。
四、重写View来实现全新的控件
当Android系统原生的控件无法满足我们的需求时,我们就可以完全创建一个新的自定义View来实现需要的功能。
重写View首先需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。当然,我们还可以像实现组合控件方式那样,通过引入自定义属性,丰富自定义View的可定制性。
具体步骤和前面差不多,主要还是自定义组件类、布局文件、自定义属性文件等,在此就不再举例具体说明了。
五、总结
最后,总结一下自定义控件的一些重要部分和步骤。
总的来说,自定义控件,首先需要有一个自定义的控件类,它继承了现有的控件或者View类或者View的子类,实现了自定义控件的一些属性和功能。
其次,对于一些需要自定义属性的控件,需要定义一个属性文件,它表明了自定义控件的引用的名字,各属性的名字及类型等信息。
最后,便是在布局文件和Activity中使用自定义控件。