自定义 View 实战一 - 轻松显示星级

需求

前面几篇文章主要都是在介绍一些自定义 View 的基础知识,本篇就来一起编写一个小 Demo,来感受感受。

自定义 View 的编写,来源于产品的无理需求,有了需求,首先是要看现有的控件能否满足需求,或者控件的组合能否满足,现有的控件满足的话,就不必去造一个轮子,费时费力。再有,考虑产品的开发周期和开发质量,周期允许,质量要求较高,那么需要考虑使用自定义 View,能够带来性能上的提升。还有一点,如果类似的 View 有重复使用的情况,也要考虑使用自定义 View。

好了,下面就来一起试试一个简单的自定义 View,一个用于展示用户等级的视图。

这里给出一个简单的设计过程,有需求开始,然后根据需求定义出设计的细节,这些确定之后,考虑我们自定义 View 的具体功能实现,完成相应的需求。

start_level_view_design

通过上述需求,整理一下:

  • 需要有一些可选项可供设置(图片,大小,间隔,最大等级等)

  • 可以动态改变等级,根据等级重复绘制,显示等级,需要有接口供调用

效果和 QQ 的等级类似,显示等级

qq

完成这个功能,需要有基础的选项设置,这些我们可以在自定义属性中设置,另外在代码中提供一些接口,在等级变化时来调用。

实现

构造函数选择

我们知道自定义 View 有 4 个:

public void View(Context context) {}
public void View(Context context, AttributeSet attrs) {}
public void View(Context context, AttributeSet attrs, int defStyleAttr) {}
public void View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}

常用的是前两个,第一个使用代码动态创建 View,第二个允许我们使用 xml 读取一些属性。所以对于这个自定义 View使用 xml 比较合适,可以允许使用者在 布局文件中做基础的设置。

自定义属性

既然允许使用者在布局文件中设置属性,那么就需要我们自定义一些属性,提供选项。 自定义属性,需要在 res 文件夹下创建一个 attrs.xml 文件,然后在这个文件中设置,定义属性。关于自定义属性这部分不具体讲了,可以谷歌一下,或者看后面给出的参考文章,写的很好,学习一下,应该没问题。定义了这些属性就可以在 xml 文件中设置相应的值。

<!--StarLevelView 属性定义-->
<declare-styleable name="StarLevelView">
<attr name="level" format="integer" /><!--设置等级-->
<attr name="drawable" format="reference" /><!--图片-->
<attr name="drawable_height" format="integer" /><!--设置图片高度,方形图-->
</declare-styleable>

读取属性

设置属性后,需要通过读取相应的值,然后做处理。在构造函数中,一般完成 Paint 的设置,以及属性值的读取等,为后面绘制过程做准备。


public StarLevelView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;

// 获取自定义属性样式列表
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StarLevelView);

level = typedArray.getInt(R.styleable.StarLevelView_level, 1);
bitMapHeight = typedArray.getInt(R.styleable.StarLevelView_drawable_height, 20);
drawableResId = typedArray.getResourceId(R.styleable.StarLevelView_drawable, R.drawable.level_star);

starBitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// dp to px
starBitmap = setImgSize(starBitmap, Util.dp2px(context, bitMapHeight), Util.dp2px(context, bitMapHeight));

typedArray.recycle();
}

注意:这里有一个图片大小转化的过程,可以自己根据需要进行设置。

public Bitmap setImgSize(Bitmap bm, int newWidth, int newHeight) {
// 获得图片的宽高
int width = bm.getWidth();
int height = bm.getHeight();
// 计算缩放比例
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// 取得想要缩放的matrix参数
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
// 得到新的图片
return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
}

大小测量

在绘制之前,需要指定这个 View 需要有多大,才能满足能够装下等级星星的图片和间隔,也就测量的过程。如果对 View 的测量过程还不熟悉,可以一起稍微学习,这部分后面会进行详细的分析。

process_view

view 的绘制就是这样一个过程,在这个 Demo 中我们需要完成 onMeasure 过程,这个过程决定该 View 的尺寸大小。测量过程中,有几种情况,根据 View 的测量模式来决定: 其实主要就是两种;

一种是自己设置了尺寸,这种情况比较好处理, View 的大小就是设定的尺寸;

// 宽和高都是设定的尺寸
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);

另一种值没有具体的数值,我们在 xml 布局中使用了 wrap_content 属性,这时就需要计算一下。 这种情况的测量模式是由父布局和子布局一起决定的,先给出这样一张图:

onMeasure

// 宽度 = (图标宽度 + 间隔)* 数量 + 宽度/2 + paddingLeft + paddingRight
viewWidth = starBitmap.getWidth() * level + starBitmap.getWidth() / 3 * (level - 1)
+ getPaddingStart() + getPaddingEnd();

// 高度 = 图片高度 + getPaddingTop() + getPaddingBottom();
viewHeight = starBitmap.getHeight() + getPaddingTop() + getPaddingBottom();

绘制

测量完之后,就可以进行绘制了,这里做了简化,等级大小实际上就是星星的个数,所以遍历循环绘制就可以了。遍历的次数也就是等级的大小 level。level 可以通过代码设置,向外提供了一个接口。

// 图片之间的横向间隔
int bitmapPadding = starBitmap.getWidth() / 3;
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < level; i++) {
// 绘制星星图标,(横坐标为图片宽度+相邻两张图片的间隔)*i+整体左边的padding值,纵坐标为view的高度/2-整体的padding值
canvas.drawBitmap(starBitmap, (starBitmap.getWidth() + bitmapPadding) * i + left, top, bitmapPaint);
}

这样就绘制完了。

如果外部通过代码设置 level 的话,还需要一个对外方法

public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
requestLayout();
}

requestLayout() 执行后,会重新走 onMeasure,onLayout,onDraw,为什么要执行 onMeasure,onLayout 这两个过程呢?因为 level 更改后,星星的个数就变化了,所以需要重新计算 View 的大小。

这不能使用 invalidate() 方法,它只是执行 onDraw 方法,重新绘制,但是不会重新测量和布局。

使用

在布局文件中引入自定义 View,注意,需要有包名

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">

<wang.ralf.customview_startlevel.StarLevelView
android:id="@+id/star_level_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingTop="10dp"
android:layout_marginTop="50dp"
app:drawable="@drawable/level_star"
app:drawable_height="40"
app:level="3" />

<Button
android:id="@+id/upgrade_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_gravity="center_horizontal"
android:layout_marginStart="8dp"
android:layout_marginTop="52dp"
android:text="升级" />

</LinearLayout>

简单的模拟一下升级过程,通过按钮点击增肌 level 值

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private int i = 1;
private StarLevelView starLevelView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
starLevelView = findViewById(R.id.star_level_view);
starLevelView.setLevel(1);
findViewById(R.id.upgrade_btn).setOnClickListener(this);
}

@Override
public void onClick(View v) {
starLevelView.setLevel(++i % 6);
}
}

Start_Level

以上就是一个简单的自定义 View,这很多东西都做了简化,联系的童鞋可以自己完善一下,也可以自己再加一些功能或者样式,比如加上类似于 ReekBar 那种虚线框星星,如果会动画,可以加上动画特效等。

初学者可能对测量侧过程有点不理解,慢慢来,多看看技术博客,多练习体会,就能够学会了,学习是一个螺旋上升的过程,对于多数人来说,当然,如果是那种看一遍就会的大神例外。后面对 View 的 onMeasure,onLayout,onDraw 这三个过程会详细分析!

参考

Android:自定义view之自定义属性

Android 深入理解Android中的自定义属性

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦