Android小白进阶(三)–自定义控件之自定义View
✍ Author by NamCooper
一、系列简介
- 1、准备整理一些自己一路走过来学习到的、日常开发中比较常用的东西。
- 2、看别人都写了那么多博客,好羡慕。
- 3、本人并不会多么高深的东西,所以写一些入门的基础知识而已,如果你是大神,请轻轻地鄙视我 。
- 4、本系列都以均以实际代码演示为主,不会涉及多少专业的名词术语。小白文,仅此而已。
二、本文内容
- 自定义控件中的中等难度的控件——自定义ViewGroup,上一篇所讲的组合控件在严格意义上来说也属于本文所讲得控件,不过ViewGroup所涉及的API更多,也需要开发者对于Android控件的布置形式有一个比较深入的理解。
三、概念介绍
- 1、自定义控件: 移动端开发的主要目的是展示和交互,那么我们在学习之初就会接触到很多种类的控件,例如Button、TextView、ImageView、ListView等等我们称之为Android原生控件的东西。
而在实际的开发中,这些原生控件固然很好用,但是不够灵活,往往不能实现我们所需要的特殊效果,这个时候我们就需要根据自己的实际需要开发出一个合适的控件来用,也就是自定控件。
自定义控件整体上可以分为三大类,由实现的难易程度排序为:组合控件、继承ViewGroup控件、继承View控件 。
有的大神可能会把组合控件归为继承ViewGroup的控件,当然这没有错,不过对于一个刚入门的小白来说,组合控件这种基本形式的自定义控件非常容易理解,所以在这里我单独列出来,作为开胃菜。 - 2、自定义View: 个人认为完全自定义View是自定义控件最难的部分,在编写这一类控件的时候,需要充分考虑该View什么时候测量完成/怎么测量,又该在什么时候把进行绘制/重绘,绘制特殊效果、动画又要怎么做。总之,涉及Api很多,对于Android坐标系统的理解需要很深。
本文使用一个简单的自定义ProgressBar作为示例。
四、实现演练
1、自定义ProgressBar
1> 效果图如下:
![](https://i-blog.csdnimg.cn/blog_migrate/c5cddfbcd7ed450a3a262f5a1e8e4c4b.png)
2> 实现细节:
①分析结构:
做一个完全自定义View,熟悉api当然是很重要的,但是明确实现结果和步骤同样非常关键。按照上面的效果图,我们可以很容易分析出,这个自定义View的绘制分为3个部分:底层矩形框(灰色框)、进度矩形框(红色框)、文本,而这三种结构在View的onDraw方法中,可以使用canvas的两个方法来实现:canvas.drawRect(),canvas.drawText()。
①代码实现,自定义一个RectProgressBar继承View,实现步骤见代码注释:
public class RectProgressBar extends View {
private Context mContext;
//可配置项
private int bgColor = Color.GRAY;
private int upColor = Color.RED;
private int txtColor = Color.GREEN;
private int pro_width = 8;
private int txtSize = 15;
private int pro_num = 0;
private Paint bgPaint;
private Paint upPaint;
private Paint txtPaint;
private int height;
private int width;
private Rect rect;
private float top;
private float bottom;
private int baseLineY = -1;
public RectProgressBar(Context context) {
this(context, null);
}
public RectProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public RectProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
bgPaint = new Paint();
bgPaint.setAntiAlias(true);
bgPaint.setColor(bgColor);
bgPaint.setStyle(Paint.Style.STROKE);
bgPaint.setStrokeWidth((float) (dip2px(mContext, pro_width * 2)));
upPaint = new Paint();
upPaint.setAntiAlias(true);
upPaint.setColor(upColor);
upPaint.setStyle(Paint.Style.FILL);
txtPaint = new Paint();
txtPaint.setAntiAlias(true);
txtPaint.setColor(txtColor);
txtPaint.setTextSize((float) (dip2px(mContext, txtSize)));
//该方法即为设置基线上那个点究竟是left,center,还是right 这里我设置为center
txtPaint.setTextAlign(Paint.Align.CENTER);
Paint.FontMetrics fontMetrics = txtPaint.getFontMetrics();
//为基线到字体上边框的距离
top = fontMetrics.top;
//为基线到字体下边框的距离
bottom = fontMetrics.bottom;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
//计算文字所处的
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (rect == null) {
rect = new Rect(0, 0, width, height);
}
if (baseLineY < 0) {
baseLineY = (int) (rect.centerY() - top / 2 - bottom / 2);//基线中间点的y轴计算公式
}
//画背景条
canvas.drawRect(rect, bgPaint);
//画文字
canvas.drawText(pro_num + "%", rect.centerX(), baseLineY, txtPaint);
//画进度
if (pro_num >= 0 && pro_num <= 25) {//0-25进度,只画上边
drawTop(canvas, pro_num);
} else if (pro_num > 25 && pro_num <= 50) {//25-50,画上边和右边
drawTop(canvas, 25);
drawRight(canvas, pro_num);
} else if (pro_num > 50 && pro_num <= 75) {//50-75,除了左边都要画
drawTop(canvas, 25);
drawRight(canvas, 50);
drawBottom(canvas, pro_num);
} else if (pro_num > 70 && pro_num <= 100) {//75-100,四条边都要画
drawTop(canvas, 25);
drawRight(canvas, 50);
drawBottom(canvas, 75);
drawLeft(canvas, pro_num);
} else {
throw new RuntimeException("the rate must between 0 and 100");
}
}
/**
* 设置背景框颜色
* @param color
*/
public void setBgColor(int color) {
bgColor = color;
bgPaint.setColor(bgColor);
invalidate();
}
/**
* 设置进度条颜色
* @param color
*/
public void setUpColor(int color) {
upColor = color;
upPaint.setColor(upColor);
invalidate();
}
/**
* 设置文本颜色
* @param color
*/
public void setTxtColor(int color) {
txtColor = color;
txtPaint.setColor(txtColor);
invalidate();
}
/**
* 设置进度条宽度
* @param widthDp dp单位
*/
public void setPro_width(int widthDp) {
pro_width = widthDp;
bgPaint.setStrokeWidth((float) (dip2px(mContext, pro_width * 2)));
invalidate();
}
/**
* 设置文本字体大小
* @param sizeSp sp单位
*/
public void setTextSize(int sizeSp) {
txtSize = sizeSp;
txtPaint.setTextSize((float) (dip2px(mContext, txtSize)));
invalidate();
}
/**
* 设置进度
* @param pro_num
*/
public void setPro_num(int pro_num) {
this.pro_num = pro_num;
invalidate();
}
private void drawLeft(Canvas canvas, float pro_num) {
canvas.drawRect(
0
, (1 - (pro_num - 75) * 1f / 25) * (height - (float) (dip2px(mContext, pro_width)))
, (float) (dip2px(mContext, pro_width))
, height - (float) (dip2px(mContext, pro_width))
, upPaint);
}
private void drawBottom(Canvas canvas, float pro_num) {
canvas.drawRect(
(1 - ((pro_num - 50) * 1f / 25)) * (width - (float) (dip2px(mContext, pro_width)))
, height - (float) (dip2px(mContext, pro_width))
, width - (float) (dip2px(mContext, pro_width))
, height
, upPaint);
}
private void drawRight(Canvas canvas, float pro_num) {
canvas.drawRect(
width - (float) (dip2px(mContext, pro_width))
, (float) (dip2px(mContext, pro_width))
, width
, (pro_num - 25) * 1f / 25 * (height - (float) (dip2px(mContext, pro_width))) + (float) (dip2px(mContext, pro_width))
, upPaint);
}
private void drawTop(Canvas canvas, float pro_num) {
canvas.drawRect((float) (dip2px(mContext, pro_width))
, 0
, (pro_num * 1f / 25 * (width - (float) (dip2px(mContext, pro_width)))) + (float) (dip2px(mContext, pro_width))
, (float) (dip2px(mContext, pro_width))
, upPaint);
}
public double dip2px(Context context, double dpValue) {
float density = context.getResources().getDisplayMetrics().density;
return dpValue * (double) density + 0.5D;
}
}
3> 核心api说明:
canvas.drawRect(left,top,right,bottom,paint):
-left:左上点x坐标
-top:左上点y坐标
-right:右下点x坐标
-bottom:右下点y坐标
-paint:用于绘制矩形的画笔
canvas.drawText(text,x,y,paint):
该方法的详细介绍请参见大神作品android canvas drawText()文字居中。
4> 使用:
<?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.namcooper.widgetdemos.activity.RectProgressBarActivity">
<com.namcooper.widgetdemos.widget.RectProgressBar
android:id="@+id/rpb_pb"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"/>
<Button
android:onClick="startAnim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="20dp"
android:text="开启动画"/>
</RelativeLayout>
5> 调用:
public class RectProgressBarActivity extends AppCompatActivity {
private RectProgressBar progressBar;
private Timer timer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rect_progress_bar);
progressBar = (RectProgressBar) findViewById(R.id.rpb_pb);
progressBar.setBgColor(Color.BLACK);
progressBar.setUpColor(Color.GREEN);
progressBar.setPro_width(20);
progressBar.setTextSize(20);
progressBar.setTxtColor(Color.RED);
}
public void startAnim(View v) {
ValueAnimator animator = ValueAnimator.ofInt(0, 100);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int x = (int) animation.getAnimatedValue();
progressBar.setPro_num(x);
}
});
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(5000);
animator.start();
}
}
6> 效果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7236a8bfcf26617672cb91ed293ecf47.png)