Android自定义View实现星星评分效果

作者:newki 时间:2023-10-27 23:10:42 

前言

在前面的学习中,我们基本了解了一些 Canvas 的绘制,那么这一章我们一起复习一下图片的绘制几种方式,和事件的简单交互方式。

我们从易到难,作为基础的进阶控件,我们从最简单的交互开始,那就自定义一个星星评分的控件吧。

一个 App 必不可少的评论系统打分的控件,可以展示评分,可以点击评分,可以滑动评分。它的实现总体上可以分为以下的步骤:

  • 强制测量大小为我们指定的大小

  • 先绘制Drawable未评分的图片

  • 在绘制Bitmap已评分的图片

  • 在onTouch中点击和移动的事件中动态计算当前的评分,进而刷新布局

  • 回调的处理与属性的抽取

思路我们已经有了,下面一步一步的来实现吧。

话不多说,Let's go

1、测量与图片的绘制

我们需要绘制几个星星,那么我们必须要设置的几个属性:

当前的评分值,总共有几个星星,每一个星星的间距和大小,选中和未选中的Drawable图片:

private int mStarDistance = 0;
   private int mStarCount = 5;
   private int mStarSize = 20;    //每一个星星的宽度和高度是一致的
   private float mScoreNum = 0.0F;  //当前的评分值
   private Drawable mStarScoredDrawable;  //已经评分的星星图片
   private Drawable mStarUnscoredDrawable;  //还未评分的星星图片

private void init(Context context, AttributeSet attrs) {

mScoreNum = 2.1f;
       mStarSize = context.getResources().getDimensionPixelSize(R.dimen.d_20dp);
       mStarDistance = context.getResources().getDimensionPixelSize(R.dimen.d_5dp);
       mStarScoredDrawable = context.getResources().getDrawable(R.drawable.iv_normal_star_yellow);
       mStarUnscoredDrawable = context.getResources().getDrawable(R.drawable.iv_normal_star_gray);
   }

测量布局的时候,我们就不能根据xml设置的 match_parent 或 wrap_content 来设置宽高,我们需要根据星星的大小与间距来动态的计算,所以不管xml中如何设置,我们都强制性的使用我们自己的测量。

星星的数量 * 星星的宽度再加上中间的间距 * 数量-1,就是我们的控件宽度,控件高度则是星星的高度。

具体的确定测量我们再上一篇已经详细的复习过了,这里直接贴代码:

@Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       setMeasuredDimension(mStarSize * mStarCount + mStarDistance * (mStarCount - 1), mStarSize);
   }

这样就可以得到对应的测量宽高 (加一个背景方便看效果):

Android自定义View实现星星评分效果

如何绘制星星?直接绘制Drawable即可,默认的Drawable的绘制为:

@Override
   protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

for (int i = 0; i < mStarCount; i++) {
           mStarUnscoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
           mStarUnscoredDrawable.draw(canvas);
       }

}

如果有5个星星图片,那么就为每一个星星定好位置:

Android自定义View实现星星评分效果

那么已经选中的图片也需要使用这种方法绘制吗?

计算当前的评分,然后计算计算需要绘制多少星星,那么就是这样做:

int score = (int) Math.ceil(mScoreNum);
   for (int i = 0; i < score; i++) {
       mStarScoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
       mStarScoredDrawable.draw(canvas);
   }

Android自定义View实现星星评分效果

可是这么做不符合我们的要求啊 ,我们是需要是可以显示评分为2.5之类值,那么我们怎么能绘制半颗星呢?Drawable.draw(canvas) 的方式满足不了,那我们可以使用 BitmapShader 的方式来绘制。

初始化一个 BitmapShader 设置给 Paint 画笔,通过画笔就可以画出对应的形状。

比如此时的场景,我们如果想只画0.5个星星,那么我们就可以

paint = new Paint();
   paint.setAntiAlias(true);
   paint.setShader(new BitmapShader(drawableToBitmap(mStarScoredDrawable), BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));

@Override
   protected void onDraw(Canvas canvas) {
       for (int i = 0; i < mStarCount; i++) {
           mStarUnscoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
           mStarUnscoredDrawable.draw(canvas);
       }

canvas.drawRect(0, 0, mStarSize * mScoreNum, mStarSize, paint);
   }

Android自定义View实现星星评分效果

那么如果是大于一个星星之后的小数点就可以用公式计算

if (mScoreNum > 1) {
       canvas.drawRect(0, 0, mStarSize, mStarSize, paint);

if (mScoreNum - (int) (mScoreNum) == 0) {
           //如果评分是3.0之类的整数,那么直接按正常的rect绘制
           for (int i = 1; i < mScoreNum; i++) {
               canvas.translate(mStarDistance + mStarSize, 0);
               canvas.drawRect(0, 0, mStarSize, mStarSize, paint);
           }
       } else {
           //如果是小数例如3.5,先绘制之前的3个,再绘制后面的0.5
           for (int i = 1; i < mScoreNum - 1; i++) {
               canvas.translate(mStarDistance + mStarSize, 0);
               canvas.drawRect(0, 0, mStarSize, mStarSize, paint);
           }
           canvas.translate(mStarDistance + mStarSize, 0);
           canvas.drawRect(0, 0, mStarSize * (Math.round((mScoreNum - (int) (mScoreNum)) * 10) * 1.0f / 10), mStarSize, paint);
       }

} else {
       canvas.drawRect(0, 0, mStarSize * mScoreNum, mStarSize, paint);
   }

效果:

Android自定义View实现星星评分效果

关于 BitmapShader 的其他用法,可以翻看我之前的自定义圆角圆形View,和自定义圆角容器的文章,里面都有用到过,主要是方便一些图片的裁剪和缩放等。

2、事件的交互与计算

这里并没有涉及到什么事件嵌套,拦截之类的复杂处理,只需要处理自身的 onTouch 即可。而我们需要处理的就是按下的时候和移动的时候评分值的变化。

在onDraw方法中,我们使用 mScoreNum 变量来绘制的已评分的 Bitmap 绘制。所以这里我们只需要在 onTouch 中计算出对应的 mScoreNum 值,让其重绘即可。

@Override
   public boolean onTouchEvent(MotionEvent event) {

//x轴的宽度做一下最大最小的限制
       int x = (int) event.getX();
       if (x < 0) {
           x = 0;
       }
       if (x > mMeasuredWidth) {
           x = mMeasuredWidth;
       }

switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN:
           case MotionEvent.ACTION_MOVE: {
               mScoreNum = x * 1.0f / (mMeasuredWidth * 1.0f / mStarCount);
               invalidate();
               break;
           }
           case MotionEvent.ACTION_UP: {
               break;
           }
       }

return super.onTouchEvent(event);

}

计算出一颗星的长度,然后计算当前x轴的长度,就可以计算出当前有几颗星,我们默认处理的是 float 类型。就可以根据计算出的 mScoreNum 值来得到对应的动画效果:

Android自定义View实现星星评分效果

3. 回调处理与自定义属性抽取

到此效果的实现算是结束了,但是我们还有一些收尾工作没做,如何监听进度的回调,如何控制整数与浮点数的显示,是否支持触摸等等。然后对其做一些自定义属性的抽取,就可以在应用中比较广泛的使用了。

自定义属性:

private int mStarDistance = 5;
   private int mStarCount = 5;
   private int mStarSize = 20;    //每一个星星的宽度和高度是一致的
   private float mScoreNum = 0.0F;  //当前的评分值
   private Drawable mStarScoredDrawable;  //已经评分的星星图片
   private Drawable mStarUnscoredDrawable;  //还未评分的星星图片
   private boolean isOnlyIntegerScore = false;  //默认显示小数类型
   private boolean isCanTouch = true; //默认支持控件的点击
   private OnStarChangeListener onStarChangeListener;

自定义属性的赋值与初始化操作:

private void init(Context context, AttributeSet attrs) {
       setClickable(true);
       TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.StarScoreView);
       this.mStarDistance = mTypedArray.getDimensionPixelSize(R.styleable.StarScoreView_starDistance, 0);
       this.mStarSize = mTypedArray.getDimensionPixelSize(R.styleable.StarScoreView_starSize, 20);
       this.mStarCount = mTypedArray.getInteger(R.styleable.StarScoreView_starCount, 5);
       this.mStarUnscoredDrawable = mTypedArray.getDrawable(R.styleable.StarScoreView_starUnscoredDrawable);
       this.mStarScoredDrawable = mTypedArray.getDrawable(R.styleable.StarScoreView_starScoredDrawable);
       this.isOnlyIntegerScore = mTypedArray.getBoolean(R.styleable.StarScoreView_starIsTouchEnable, true);
       this.isOnlyIntegerScore = mTypedArray.getBoolean(R.styleable.StarScoreView_starIsOnlyIntegerScore, false);
       mTypedArray.recycle();

paint = new Paint();
       paint.setAntiAlias(true);
       paint.setShader(new BitmapShader(drawableToBitmap(mStarScoredDrawable), BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
   }

自定义属性的定义xml文件:

<!--  评分星星控件  -->
   <declare-styleable name="StarScoreView">
       <!--星星间距-->
       <attr name="starDistance" format="dimension" />
       <!--星星大小-->
       <attr name="starSize" format="dimension" />
       <!--星星个数-->
       <attr name="starCount" format="integer" />
       <!--星星已评分图片-->
       <attr name="starScoredDrawable" format="reference" />
       <!--星星未评分图片-->
       <attr name="starUnscoredDrawable" format="reference" />
       <!--是否可以点击-->
       <attr name="starIsTouchEnable" format="boolean" />
       <!--是否显示整数-->
       <attr name="starIsOnlyIntegerScore" format="boolean" />
   </declare-styleable>

在OnTouch的时候就可以判断是否能触摸

@Override
   public boolean onTouchEvent(MotionEvent event) {
       if (isCanTouch) {

//x轴的宽度做一下最大最小的限制
           int x = (int) event.getX();
           if (x < 0) {
               x = 0;
           }
           if (x > mMeasuredWidth) {
               x = mMeasuredWidth;
           }

switch (event.getAction()) {
               case MotionEvent.ACTION_DOWN:
               case MotionEvent.ACTION_MOVE: {
                   setStarMark(x * 1.0f / (getMeasuredWidth() * 1.0f / mStarCount));
                   break;
               }
               case MotionEvent.ACTION_UP: {
                   break;
               }
           }

return super.onTouchEvent(event);

} else {
           //如果设置不能点击,直接不触发事件
           return false;
       }

}

而 setStarMark 则是设置入口的方法,内部判断是否支持小数点和设置对于的监听,并调用重绘。

public void setStarMark(float mark) {
       if (isOnlyIntegerScore) {
           mScoreNum = (int) Math.ceil(mark);
       } else {
           mScoreNum = Math.round(mark * 10) * 1.0f / 10;
       }
       if (this.onStarChangeListener != null) {
           this.onStarChangeListener.onStarChange(mScoreNum);  //调用监听接口
       }
       invalidate();
   }

一个简单的图片绘制和事件触摸的控件就完成啦,使用起来也是超级方便。

<com.guadou.kt_demo.demo.demo18_customview.star.StarScoreView
       android:id="@+id/star_view"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_horizontal"
       android:layout_marginTop="@dimen/d_40dp"
       android:background="#f1f1f1"
       app:starCount="5"
       app:starDistance="@dimen/d_5dp"
       app:starIsOnlyIntegerScore="false"
       app:starIsTouchEnable="true"
       app:starScoredDrawable="@drawable/iv_normal_star_yellow"
       app:starSize="@dimen/d_35dp"
       app:starUnscoredDrawable="@drawable/iv_normal_star_gray" />

Activity中可以设置评分和设置监听:

override fun init() {

val starView = findViewById<StarScoreView>(R.id.star_view)

starView.setOnStarChangeListener {
           YYLogUtils.w("当前选中的Star:$it")
       }

findViewById<View>(R.id.set_progress).click {
           starView.setStarMark(3.5f)
       }
   }

效果:

Android自定义View实现星星评分效果

后记

整个流程走下来是不是很简单呢,此控件不止用于星星类型的评分,任何图片资源都可以使用,现在我们思路打开扩展一下,相似的场景和效果我们可以实现一些图片进度,触摸进度条,圆环的SeekBar,等等类似的控制都是相似的思路。

来源:https://juejin.cn/post/7167256092051767326

标签:Android,自定义,View,评分
0
投稿

猜你喜欢

  • java通过Jsoup爬取网页过程详解

    2021-12-20 03:24:10
  • 使用chatgpt实现微信聊天小程序的代码示例

    2022-04-26 17:18:24
  • java中拼接字符串的5种方法效率对比

    2022-01-08 05:46:18
  • C# DES加密算法中向量的作用详细解析

    2022-07-13 07:49:48
  • android使用AsyncTask实现多线程下载实例

    2023-02-02 16:30:04
  • Android网格布局GridView实现漂亮的多选效果

    2023-10-20 08:52:38
  • 基于@PathVariable注解的用法说明

    2023-10-01 17:22:55
  • 使用SSM+Layui+Bootstrap实现汽车维保系统的示例代码

    2023-11-28 18:30:04
  • java字符串与日期类型转换的工具类

    2021-12-30 02:44:30
  • 探讨如何用委托处理排序

    2023-12-17 15:06:36
  • 详解Spring Boot Profiles 配置和使用

    2021-10-05 22:54:57
  • C# 骑士飞行棋的源码(分享)

    2021-10-11 02:54:56
  • SpringBoot使用JWT实现登录验证的方法示例

    2023-09-19 19:12:10
  • Java实现单例模式的五种方法介绍

    2022-10-20 17:32:56
  • 深入android Unable to resolve target 'android-XX'详解

    2023-05-29 13:21:33
  • 浅谈java安全编码指南之死锁dead lock

    2023-06-22 04:49:19
  • 详解Spring Boot自动装配的方法步骤

    2023-11-18 15:02:18
  • C#中内联函数的用法介绍

    2023-04-18 18:29:18
  • MyBatis分页插件PageHelper的使用与原理

    2021-06-15 09:24:35
  • Android 监听手机GPS打开状态实现代码

    2022-09-28 08:56:38
  • asp之家 软件编程 m.aspxhome.com