需求
前面几篇文章主要都是在介绍一些自定义 View 的基础知识,本篇就来一起编写一个小 Demo,来感受感受。
自定义 View 的编写,来源于产品的无理需求,有了需求,首先是要看现有的控件能否满足需求,或者控件的组合能否满足,现有的控件满足的话,就不必去造一个轮子,费时费力。再有,考虑产品的开发周期和开发质量,周期允许,质量要求较高,那么需要考虑使用自定义 View,能够带来性能上的提升。还有一点,如果类似的 View 有重复使用的情况,也要考虑使用自定义 View。
好了,下面就来一起试试一个简单的自定义 View,一个用于展示用户等级的视图。
这里给出一个简单的设计过程,有需求开始,然后根据需求定义出设计的细节,这些确定之后,考虑我们自定义 View 的具体功能实现,完成相应的需求。
![](https://img.haomeiwen.com/i4744186/afa6861c81cb39ff.png)
通过上述需求,整理一下:
需要有一些可选项可供设置(图片,大小,间隔,最大等级等)
可以动态改变等级,根据等级重复绘制,显示等级,需要有接口供调用
效果和 QQ 的等级类似,显示等级
![](https://img.haomeiwen.com/i4744186/341bbc843b829e16.jpeg)
完成这个功能,需要有基础的选项设置,这些我们可以在自定义属性中设置,另外在代码中提供一些接口,在等级变化时来调用。
实现
构造函数选择
我们知道自定义 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 的测量过程还不熟悉,可以一起稍微学习,这部分后面会进行详细的分析。
![](https://img.haomeiwen.com/i4744186/32899f07ddadd6cc.png)
view 的绘制就是这样一个过程,在这个 Demo 中我们需要完成 onMeasure 过程,这个过程决定该 View 的尺寸大小。测量过程中,有几种情况,根据 View 的测量模式来决定:
其实主要就是两种;
一种是自己设置了尺寸,这种情况比较好处理, View 的大小就是设定的尺寸;
// 宽和高都是设定的尺寸
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
另一种值没有具体的数值,我们在 xml 布局中使用了 wrap_content 属性,这时就需要计算一下。
这种情况的测量模式是由父布局和子布局一起决定的,先给出这样一张图:
![](https://img.haomeiwen.com/i4744186/2a286d45f31c7b42.png)
// 宽度 = (图标宽度 + 间隔)* 数量 + 宽度/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);
}
}
![](https://img.haomeiwen.com/i4744186/c3d1e991360249e9.jpg)
以上就是一个简单的自定义 View,这很多东西都做了简化,联系的童鞋可以自己完善一下,也可以自己再加一些功能或者样式,比如加上类似于 ReekBar 那种虚线框星星,如果会动画,可以加上动画特效等。
初学者可能对测量侧过程有点不理解,慢慢来,多看看技术博客,多练习体会,就能够学会了,学习是一个螺旋上升的过程,对于多数人来说,当然,如果是那种看一遍就会的大神例外。后面对 View 的 onMeasure,onLayout,onDraw 这三个过程会详细分析!
网友评论