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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011371324/article/details/87904772

需求

前面几篇文章主要都是在介绍一些自定义 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中的自定义属性

猜你喜欢

转载自blog.csdn.net/u011371324/article/details/87904772
今日推荐