目录
Android自定义控件
ref:
当系统控件不能满足我们的需求的时候,这时候我们就需要自定义控件,根据我们的需求来定制一个能满足我们需求的控件。
如果预构建的微件或布局都不能满足您的需求,您可以创建自己的 View 子类。如果您只需要对现有微件或布局进行细微调整,则只需将相应微件或布局子类化并替换其方法即可。
在自定义View时,通常会重写onDraw()
方法来绘制View的显示内容。
如果该View还需要使用wrap_content
属性,那么还必须重写onMeasure()
方法。
通过自定义attrs
属性,还可以设置新的属性配置值。
自定义View的时候,并不需要重写所有的方法,只需要重写特定条件的回调方法即可。
在View中通常有以下一些比较重要的回调方法:
onFinishInflate()
:从XML加载组件后回调。onSizeChanged()
:组件大小改变时回调。onMeasure()
:回调该方法来进行测量。onLayout()
:回调该方法来确定显示的位置。onTouchEvent()
:监听到触摸事件时回调。
在通常情况下,有三种方法来实现自定义的控件:
- 对现有控件进行拓展
- 创建复合控件
- 重写View来实现全新控件
一、对现有控件进行扩展
在原生控件的基础上进行拓展,增加新的功能、修改显示的UI等
二、创建复合控件
继承一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件,创建出具体重用功能的控件集合
以一个通用的TopBar为示例
public class TopBar extends RelativeLayout {
public TopBar(Context context) {
this.TopBar(context, null);
}
public TopBar(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化的方法
// 初始化属性
initAttr(context, attrs)
// 初始化布局
initView(context);
// 初如化事件
initEvent();
}
}
1 定义属性
为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件,并在该文件中通过如下代码定义相应的属性即可。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar"> <!-- 确定引用的名称 -->
<!-- 定义title文字,大小,颜色 -->
<attr name="title" format="string" /> <!-- attr 标签来声明具体的自定义属性,name声明属性名,format来指定属性的类型 -->
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<!-- 定义left 文字,大小,颜色,背景 -->
<attr name="leftTextColor" format="color" />
<attr name="leftTextSize" format="dimension" />
<!-- 表示背景可以是颜色,也可以是引用 -->
<attr name="leftBackground" format="reference|color" />
<attr name="leftText" format="string" />
<!-- 定义right 文字,大小,颜色,背景 -->
<attr name="rightTextColor" format="color" />
<attr name="rightTextSize" format="dimension"/>
<attr name="rightBackground" format="reference|color" /> <!-- 有些属性可以是颜色属性,也可以是引用属性。比如按键的背景,所以使用“|”来分隔不同的属性 -->
<attr name="rightText" format="string" />
</declare-styleable>
</resources>
// 在TopBar的构造方法中,通过如下所示代码来获取在XML布局文件中自定义的那些属性,即与我们使用系统提供的那些属性一样。
// TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
系统提供TypedArray来获取自定义属性集,后面引用的styleable的TopBar,就是在XML中通过所指定的name名。通过TypeArray对象的getString()、getColor()等方法,就可以获取这些定义的属性值。
需要注意的是,当获取完所有的属性值后,需要调用TypedArray的recyle()方法来完成资源的回收。
public class TopBar extends RelativeLayout {
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
private float mLeftTextSize;
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightTextSize;
private float mRightTextSize;
private String mTitleText;
private float mTitleTextSize;
private int mTitleTextColor;
private void initAttr(Context context, AttributeSet attrs) {
// 通过这个方法,将你在attrs.xml中定义的declare-styleable的所有属性的值存储到TypedArray.
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
// 从TypedArray中取出对应的值来为要设置的属性赋值
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mLeftTextSize = typed.getDimension(R.styleable.TitleBar_leftTextSize, 20);
mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
mRightText = ta.getString(R.styleable.TopBar_rightText);
mRightTextSize = typed.getDimension(R.styleable.TitleBar_rightTextSize, 20);
mTitleText = ta.getString(R.styleable.TopBar_titleText);
mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
// 获取完TypedArray的值后,一般要调用recyle()方法来避免重新创建的时候的错误
ta.recycle();
}
public TopBar(Context context) {
this.TopBar(context, null);
}
public TopBar(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化的方法
// 初始化属性
initAttr(context, attrs)
// 初始化布局
initView(context);
// 初如化事件
initEvent();
}
}
2 组合控件
TopBar由三个控件组成,左边按钮mLeftButton、右边按钮mRightButton、中间标题栏mTitleView。
通过动态添加控件的方式,使用addView()
方法将三个控件加入到定义的TopBar模板中,并给它们设置我们前面所获取到的具体的属性值,比如标题的文字、颜色、大小等,代码如下所示。
private TextView mTitleView;
private Button mLeftButton;
private Button mRightButton;
private RelativeLayout.LayoutParams mLeftParams;
private RelativeLayout.LayoutParams mRightParams;
private RelativeLayout.LayoutParams mTitleParams;
private void initView(Context context) {
mTitleView = new TextView(context);
mLeftButton = new Button(context);
mRightButton = new Button(context);
// 为创建的组件赋值,值就来源于引用的xml文件中给对应属性的赋值
mTitleView.setText(mTitleText);
mTitleView.setTextSize(mTitleTextSize);
mTitleView.setTextColor(mTitleTextColor);
mTitleView.setGravity(Gravity.CENTER);
mLeftButton.setText(mLeftText);
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackgroundDrawable(mLeftBackground);
mLeftButton.setTextSize(mLeftTextSize);
mRightButton.setText(mRightText);
mRightButton.setTextSize(mRightTextSize);
mRightButton.setBackgroundDrawable(mRightBackground);
mRightButton.setTextColor(mRightTextColor);
// 为组件元素设置相应的布局元素
// 设置布局的layout_width和layout_height属性
mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
// 该方法表示所设置节点的属性必须关联其他兄弟节点或者属性值为布尔值。
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
// 动态添加组件
addView(mLeftButton, mLeftParams);
mRightParams= new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
addView(mRightButton, mRightParams);
mTitleParams= new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
addView(mTitleView, mTitleParams);
}
作为UI模板,调用者所需要这些按钮的实现功能都是不一样的。
因此,不能直接在UI模板里实现逻辑,可以通过接口回调的思想,实现逻辑交给调用者。
-
定义接口。定义一个左右按钮点击的接口,并创建两个方法,分别用于左右两个按钮的点击
-
// 在类内部定义一个接口对象,实现回调机制,不用去考虑如何实现,具体实现由调用者去创建 public interface OnClickListener{ // 左按钮点击事件 void leftClick(); // 右按钮点击事件 void rightClick(); }
-
-
暴露接口给调用者。在模板方法中,为左右按键增加点击事件,但不实现具体逻辑,而是调用接口中相应的点击方法
-
// 创建一个接口对象 private OnClickListener mListener; // 暴露一个方法给调用者来注册接口,通过接口来获得回调者对接口方法的实现 public void setOnClickListener(OnClickListener listener) { this.mListener = listener; } private void initEvent(){ // 按钮的点击事件,不需要具体的实现,只需要调用接口方法,回调的时候会有具体实现 mLeftButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.leftClick(); } }); mRightButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.rightClick(); } }); }
-
-
实现接口回调。调用者的代码实现这样接口,确定具体的实现逻辑,并使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。通常情况下,可以使用匿名内部类的形式来实现接口中的方法
-
mTopBar.setOnClickListener(new TopBar.OnClickListener() { @Override public void leftClick() { // 点击左边按钮 } @Override public void rightClick() { // 点击右边按钮 } }
-
-
除了通过接口回调的方式来实现动态的控制UI模板,同样可以使用公共方法来动态地修改UI模板中的UI,进一步提高了模板的可定制性
-
public static final int LEFT = 1; public static final int RIGHT = 2; /** * 设置按钮的显示与否通过常量区分,visible区分是否显示 * * @param view 标记View * @param visible 是否显示 * / public void setVisable(int view, int visible){ switch(view) { case LEFT: mLeftButton.setVisibility(visible); break; case RIGHT: mRightButton.setVisibility(visible); break; } }
-
通过如上代码,调用者通过TopBar对象调用这个方法后,根据参数,可以动态地控制按钮的显示
-
// 控制TopBar上组件的状态 mTopBar.setVisable(TopBar.LEFT, View.VISIBLE); mTopBar.setVisable(TopBar.RIGHT, View.GONE);
-
-
3 引用UI模板
在引用前,需要指定引用第三方控件的名字空间
xmlns:android="http://schemas.android.com/apk/res/android"
<!--
这里指定了名字空间为“android”,因此在接下来使用系统属性时,才可以使用“android:”来引用Android的系统属性。
同样,如果要使用自定义属性,那么就需要创建自己的名字空间。
在Android Stuido中,第三方控件都使用如下代码来引用名字空间。
xmlns:app="http://schemas.android.com/apk/res/res-auto" (将引入的第三方控件的名字空间取名为app)
在XML文件中使用自定义的属性时,就可以通过这个名字空间来引用,如下
-->
<com.example.demo.TopBar
android:id="@+id/tb"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
app:leftBackGround="#ff000000"
app:leftText="Back"
app:leftTextColor="#ffff6734"
app:leftTextSize="25dp"
app:rightText="More"
app:rightTextSize="25dp"
app:rightTextColor="#ff123456"
app:title="自定义标题"
app:titleTextColor="#ff654321"/>
使用自定义的View与系统原生的View最大的区别就是在声明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmlns名字。
三、重写View来实现全新控件
Android系统原生控件无法满足我们的需求的时候,我们就可以完全创建一个新的自定义View来实现需要的功能。
- 创建一个自定义View,难点在于绘制控件和实现交互,这也是评价一个自定义View优劣的标准之一
- 需要继承View类,并重写它的
onDraw()
、onMeasure()
等方法来实现绘制逻辑 - (可选) 重写
onTouchEvent()
等触控事件来实现交互逻辑 - (可选) 引入自定义属性,丰富自定义View的可定制性
1 弧线展示图
实例:
1.1 具体步骤:
(1)绘制中间的圆形
(2)绘制圆形中间的文字
(3)绘制圆形外面的圆弧、外圈的弧线
public class ScaleMap extends View {
private int mMeasureHeigth;// 控件高度
private int mMeasureWidth;// 控件宽度
// 圆形
private Paint mCirclePaint;
private float mCircleXY;//圆心坐标
private float mRadius;//圆形半径
// 圆弧
private Paint mArcPaint;
private RectF mArcRectF;//圆弧的外切矩形
private float mSweepAngle;//圆弧的角度
private float mSweepValue;
// 文字
private Paint mTextPaint;
private String mShowText;//文本内容
private float mShowTextSize;//文本大小
public ScaleMap(Context context) {
this(context, null);
}
public ScaleMap(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleMap(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
// 如果不用后面的参数,就不需要重构后面的,直接将其内容写在第一个构造方法就可以,父类会自动执行后面的构造方法
public ScaleMap(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// 初始化操作
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);//获取控件宽度
mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec);//获取控件高度
setMeasuredDimension(mMeasureWidth, mMeasureHeigth);
initPaint(); // 画笔中用到了宽高所以在此初始化画笔
}
/**
* 准备画笔,--> 在初始化的时候,设置好绘制三种图形的参数
*/
private void initPaint() {
float length = Math.min(mMeasureWidth,mMeasureHeigth);
// 圆的代码
mCircleXY = length / 2;// 确定圆心坐标
mRadius = (float) (length * 0.5 / 2);// 确定半径
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);// 去锯齿
mCirclePaint.setColor(getResources().getColor(android.R.color.holo_green_dark));
// 弧线,需要 指定其椭圆的外接矩形
// 矩形
mArcRectF = new RectF((float) (length * 0.1), (float) (length * 0.1), (float)(length * 0.9),(float) (length * 0.9));
mSweepAngle = (mSweepValue / 100f) * 360f;
mArcPaint = new Paint();
mArcPaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));
mArcPaint.setStrokeWidth((float) (length * 0.1));//圆弧宽度
mArcPaint.setStyle(Style.STROKE);//圆弧
// 文字,只需要设置好文字的起始绘制位置即可
mShowText = "Android Skill";
mShowTextSize = 50;
mTextPaint = new Paint();
mTextPaint.setTextSize(mShowTextSize);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) { // --> 在onDraw()方法中去绘制
super.onDraw(canvas);
// 绘制圆
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
// 绘制圆弧,逆时针绘制,角度跟
canvas.drawArc(mArcRectF, 90, mSweepAngle, false, mArcPaint);
// 绘制文字
canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + mShowTextSize / 4, mTextPaint);
}
// 让调用者来设置不同的状态值,使弧形弧度变化
public void setSweepValue(float sweepValue) {
if (sweepValue != 0) {
mSweepValue = sweepValue;
} else {
mSweepValue = 25;
}
// 这个方法可以刷新UI --> 在修改UI后通过调用this.invalidate()方法来实现UI的重绘
this.invalidate();
}
}
2 音频条形图
实例:
2.1 具体步骤
(1)基本思路:绘制一个个的矩形,每个矩形之间稍微偏移一点距离即可
动态效果实现思路:在onDraw()
方法中调用invalidate()
方法通知View进行重绘
--> 问题:这样直接重绘的速度太快,影响观感效果体验,因此可以适当进行延时通知重绘:
使用postInvalidateDelayed(300/*ms*/)
来进行延时重绘
this.invalidate();
this.postInvalidateDelayed(300);
(2)矩形渐变效果
思路:给绘制的Paint对象可以增加一个LinearGradient渐变效果
private int mWidth;//控件的宽度
private int mRectWidth;// 矩形的宽度
private int mRectHeight;// 矩形的高度
private Paint mPaint;
private int mRectCount;// 矩形的个数
private int offset = 5;// 偏移
private double mRandom;
private LinearGradient lg;// 渐变
public ScaleMap(Context context) {
this(context, null);
}
public ScaleMap(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint(); // 这些要在这里设置,因为渐变效果
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置宽高
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
// 初始化画笔
private void initPaint() {
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.FILL);
mRectCount = 12;
}
//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mRectCount; i++) {
mRandom = Math.random();
float currentHeight = (int) (mRectHeight * mRandom);
canvas.drawRect((float) (mWidth * 0.4 / 2 + mRectWidth * i + offset * i), currentHeight,
(float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1) + offset * i), mRectHeight, mPaint);
}
postInvalidateDelayed(300);
}
//重写onSizeChanged方法,给画笔加上渐变
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getWidth();
mRectHeight = getHeight();
mRectWidth = (int) (mWidth * 0.6 / mRectCount);
lg = new LinearGradient(0, 0, mRectWidth, mRectHeight, Color.GREEN, Color.BLUE, TileMode.CLAMP);
mPaint.setShader(lg);
}
四、补充:自定义ViewGroup
TODO