美文网首页Android自定义View安卓UIAndroid专题
自定义View 实战一 - 轻松显示星级

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

作者: Kip_Salens | 来源:发表于2019-02-24 16:38 被阅读42次

    需求

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

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

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

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

    start_level_view_design.png

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

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

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

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

    custom_view_qq.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 的测量过程还不熟悉,可以一起稍微学习,这部分后面会进行详细的分析。

    process_view.png

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

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

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

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

    custom_view_onMeasure.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);
        }
    }
    
    
    customview_start_level.jpg

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

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

    参考

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

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

    相关文章

      网友评论

        本文标题:自定义View 实战一 - 轻松显示星级

        本文链接:https://www.haomeiwen.com/subject/ljlbyqtx.html