一、为什么要自定义View
当Android本身提供的View不能够满足我们需要实现的View的时候,我们就需要进行自定义View。与其说是在自定义一个View,不如说是在设计一个图形。自定义View通常有下面三种方法来实现:
- 对现有控件进行拓展
- 通过组合来实现新的控件
- 重写View来实现全新的控件
下面我们就来具体讲解一下:
二、对现有控件进行拓展
这是一个非常重要的自定义View的方法,它可以在原生的控件上进行拓展,进一步增加新的功能,修改显示的UI等等。一般来说我们可以在onDraw()方法中实现对原生控件的修改。
我们以TextView为例,要实现下图的效果,首先我们必须要继承自AppCompatTextView,重写构造方法后在onDraw()中对TextView进行拓展。
@Override
protected void onDraw(Canvas canvas) {
//在回调父类方法之前,实现自己的逻辑,
//对TextView来说即是在绘制文本内容之前
super.onDraw(canvas);
//在回调父类方法之后,实现自己的逻辑
//对TextView来说即是在绘制文本内容之后
}
简单的来说,对现有控件进行拓展实质上就是通过改变控件的绘制行为来创建自定义View,上图是为TextView绘制几层背景,那么很显然我们需要在系统调用 之前,改变原生的绘制行为。全部代码如下:
package com.lunbo.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
public class MyTextView extends AppCompatTextView {
private Paint mPaint1, mPaint2;
public MyTextView(Context context) {
super(context);
initView();
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint1 = new Paint();
mPaint1.setColor(Color.RED);
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawRect(
0,
0,
getMeasuredWidth(),
getMeasuredHeight(),
mPaint1
);
canvas.drawRect(
0,
0,
getMeasuredWidth() - 10,
getMeasuredHeight() - 10,
mPaint2
);
canvas.save();
canvas.translate(10, 0);
super.onDraw(canvas);
canvas.restore();
}
}
三、创建复合控件
创建复合控件可以很好的创建出具有功能的控件集合。这种方式通常需要继承一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。
对于一个应用APP,我们为了程序风格的统一,很多Activity界面都会有一些通用的UI界面,如下图所示:
在不同的界面我们希望他们某些控件存在或者消失,并且文字依照不同的应用场景进行变换,那么我们可以有以下两种方法来实现。
- 当所构建的控件能够用基本控件来进行搭建时
我们可以单独封装这个布局,在每个Activity需要用到的时候采用include来嵌入。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/MainColor"
android:paddingRight="16dp"
android:paddingLeft="16dp"
>
<ImageView
android:id="@+id/iv_back"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@mipmap/back"
android:layout_gravity="center_vertical"/>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网易云音乐"
android:textStyle="bold"
android:textColor="@android:color/white"
android:textSize="@dimen/navBarTitleSize"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/iv_me"
android:layout_width="38dp"
android:layout_height="38dp"
android:src="@mipmap/me"
android:layout_gravity="right|center_vertical"/>
</FrameLayout>
构建一个父Activity,在里面去创建一个公开方法,控制navigationBar的显示效果。具体如下:
package com.alanwade;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
public class BaseActivity extends Activity {
private ImageView ivBack, ivMe;
private TextView tvTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
//构建initNavBar方法,控制显示。
public void initNavBar(boolean isShowBack, String title, boolean isShowMe){
ivBack = findViewById(R.id.iv_back);
ivMe = findViewById(R.id.iv_me);
tvTitle = findViewById(R.id.tv_title);
tvTitle.setText(title);
ivBack.setVisibility(isShowBack ? View.VISIBLE : View.GONE);
ivMe.setVisibility(isShowMe ? View.VISIBLE : View.GONE);
ivBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
ivMe.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(BaseActivity.this,MeActivity.class));
}
});
}
}
- 将复合控件所有功能封装至一个View中
首先我们需要定义自己设置这个View的属性,在values目录下新建一个attrs的xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="title" format="string"/>
<attr name="titleTextSize" format="dimension"/>
<attr name="titleTextColor" format="color"/>
<attr name="leftTextColor" format="color"/>
<attr name="leftBackground" format="reference|color"/>
<attr name="leftText" format="string"/>
<attr name="rightTextColor" format="color"/>
<attr name="rightBackground" format="reference|color"/>
<attr name="rightText" format="string"/>
</declare-styleable>
</resources>
随后将topbar继承自RelativeLayout,重写构造方法。通过TypedArray来获取自定义的属性集合,切记用完之后要回收,具体代码如下:
package com.lunbo.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.lunbo.R;
import java.awt.font.TextAttribute;
public class TopBar extends RelativeLayout {
//控件元素,左右按钮和中间标题
private Button mLeftButton, mRightButton;
private TextView mTitleView;
//布局元素
private LayoutParams mLeftParams, mTitleParams, mRightParams;
//左按钮属性
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
//右按钮属性
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightText;
//标题属性
private float TitleTextSize;
private int mTitleColor;
private String mTitle;
//创建接口对象
private topbarClickListener mListener;
public void setmListener(topbarClickListener mListener) {
this.mListener = mListener;
}
public TopBar(Context context) {
super(context);
}
public TopBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public TopBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public TopBar(Context context, AttributeSet attrs) {
super(context, attrs);
//为TopBar设置背景颜色
setBackgroundColor(getResources().getColor(R.color.TopBarColor));
//利用TypedArray来获取自定义属性集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
mLeftTextColor = typedArray.getColor(R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = typedArray.getDrawable(R.styleable.TopBar_leftBackground);
mLeftText = typedArray.getString(R.styleable.TopBar_leftText);
mRightTextColor = typedArray.getColor(R.styleable.TopBar_rightTextColor, 0);
mRightBackground = typedArray.getDrawable(R.styleable.TopBar_rightBackground);
mRightText = typedArray.getString(R.styleable.TopBar_rightText);
TitleTextSize = typedArray.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleColor = typedArray.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitle = typedArray.getString(R.styleable.TopBar_title);
//用完typedArray一定要记得回收
typedArray.recycle();
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackground(mLeftBackground);
mLeftButton.setText(mLeftText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackground);
mRightButton.setText(mRightText);
mTitleView.setTextColor(mTitleColor);
mTitleView.setTextSize(TitleTextSize);
mTitleView.setText(mTitle);
mTitleView.setGravity(Gravity.CENTER);
//为组件设置相应的布局元素
mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
addView(mLeftButton, mLeftParams);
mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(mRightButton, mRightParams);
mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTitleView, mTitleParams);
//为button设置点击事件
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.leftclick();
}
});
mRightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.rightclick();
}
});
}
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);
}
}
}
public interface topbarClickListener{
void leftclick();
void rightclick();
}
}
三、重写View来实现全新的控件
当Android系统原生控件无法满足我们的需求时,我们就可以完全创建一个新的自定义View来实现。创建一个全新的View通常需要继承自View类,并重写它的onMeasure()和onDraw()方法来实现绘制逻辑,同时重写onTouchEvent()等触控事件来实现交互逻辑。
比如我们实现下图所示的控件:
这类控件很显然只需要去重写绘制逻辑就行,分为三个部分来绘制,第一是内层的圆圈,第二是外层的弧线,最后是内部文字显示,都通过onDraw()方法来实现,在onMeasure()中提前设置好这三个图像的绘制参数。具体代码可供参考:
package com.lunbo.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Printer;
import android.view.View;
import androidx.annotation.Nullable;
public class CircleProgressView extends View {
private int mMeacsureWidth;
private int mMeacsureHeight;
//定义内圈
private Paint mCirlePaint;
private float mCircleXY;
private float mCircleRadius;
//定义外圈
private Paint mArcCirlePaint;
private RectF mArcRectF;
private float mSweepAngle;
private float mSweepValue = 66;
//定义内部文字显示
private Paint TextPaint;
private String mText;
private float mTextSize;
public CircleProgressView(Context context) {
super(context);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
//设置绘制的参数
private void initView() {
float length = 0;
if(mMeacsureHeight >= mMeacsureWidth){
length = mMeacsureWidth;
}else{
length = mMeacsureHeight;
}
mCircleXY = length / 2;
mCircleRadius = (float) ((length * 0.5) / 2);
mCirlePaint = new Paint();
mCirlePaint.setAntiAlias(true);
mCirlePaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));
mArcRectF = new RectF(
(float) (length * 0.1),
(float) (length * 0.1),
(float) (length * 0.9),
(float) (length * 0.9)
);
mSweepAngle = (mSweepValue / 180f) * 360f;
mArcCirlePaint = new Paint();
mArcCirlePaint.setAntiAlias(true);
mArcCirlePaint.setColor(getResources().getColor(android.R.color.holo_green_light));
mArcCirlePaint.setStyle(Paint.Style.STROKE);
mArcCirlePaint.setStrokeWidth((float)(length * 0.1));
mText = setmText();
mTextSize = setmTextSize();
TextPaint = new Paint();
TextPaint.setTextSize(mTextSize);
TextPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mMeacsureWidth = MeasureSpec.getSize(widthMeasureSpec);
mMeacsureHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(mMeacsureWidth, mMeacsureHeight);
initView();
}
//分别绘制三种图像
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mCircleXY, mCircleXY, mCircleRadius, mCirlePaint);
canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcCirlePaint);
canvas.drawText(mText, 0, mText.length(),mCircleXY, mCircleXY + (mTextSize / 4), TextPaint);
}
public float setmTextSize() {
this.invalidate();
return 50;
}
public String setmText() {
this.invalidate();
return "Android View";
}
public void forceInvalidate() {
this.invalidate();
}
public void setmSweepValue(float SweepValue) {
if(SweepValue != 0){
mSweepValue = SweepValue;
}else {
mSweepValue = 70;
}
this.invalidate();
}
}
总结
适当的使用自定义View,可以丰富程序的体验效果。但滥用自定义View会带来适得其反的效果。一个让用户熟悉的控件才是好控件,祝各位能早日掌握自定义View,为你的程序添砖加瓦!