自定义View(6)评分控件RatingBar

1. 效果实现分析

官方现在已经有RatingBar控件可以直接调用了,现在是模仿官方的方式做一个,熟悉一下自定义View,如果平时使用的话,最好还是直接使用官方得比较好。自定义RatingBar,可以找到三份图,没选中的,选中一半的和完全选中的。然后将n张没有选中的放一堆表示初始状态,最后做个触摸交互,监听位置分别画选中一半和全选的。

1. 自定义View
2. 画n张没有选中的图片,表示初始状态
3. 触摸交互(这里需要监听移动就行了),监听位置分别画选中一半和全选的

在这里插入图片描述

2. 初始化显示评分控件

2.1 自定义评分控件

2.2.1 新建drawable-xhdpi

找三份图,没选中的,选中一半的和完全选中的。推荐使用 iconfont-阿里巴巴矢量图标库,图片的分辨率最好小一点,且三份分辨率一样。

在这里插入图片描述

2.2.2 新建attrs.xml

<!-- /View6/app/src/main/res/values/attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RatingBar">
        <attr name="starNormal" format="reference"/>
        <attr name="starHalf" format="reference"/>
        <attr name="starSelect" format="reference"/>
        <attr name="gradeNumber" format="integer"/>
    </declare-styleable>
</resources>

2.2.3 新建RatingBar继承View

//View6/app/src/main/java/com/example/view6/RatingBar.java
public class RatingBar extends View {
    private Bitmap mNormalBitmap, mHalfBitmap, mSelectBitmap;
    private int mGradeNumber = 5;
    private int mCurrentGradeNumber = 0;

    public RatingBar(Context context) {
        this(context, null);
    }

    public RatingBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RatingBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RatingBar);
        int starNormal = array.getResourceId(R.styleable.RatingBar_starNormal, 0);
        // 如果xml中没有给默认值,则抛出异常
        if (starNormal == 0) {
            throw new RuntimeException("请设置属性 starNormal");
        }
        mNormalBitmap = BitmapFactory.decodeResource(getResources(), starNormal);

        int starHalf = array.getResourceId(R.styleable.RatingBar_starHalf, 0);
        // 如果xml中没有给默认值,则抛出异常
        if (starHalf == 0) {
            throw new RuntimeException("请设置属性 starHalf");
        }
        mHalfBitmap = BitmapFactory.decodeResource(getResources(), starHalf);

        int starSelect = array.getResourceId(R.styleable.RatingBar_starSelect, 0);
        // 如果xml中没有给默认值,则抛出异常
        if (starSelect == 0) {
            throw new RuntimeException("请设置属性 starSelect");
        }
        mSelectBitmap = BitmapFactory.decodeResource(getResources(), starSelect);

        mGradeNumber = array.getInt(R.styleable.RatingBar_gradeNumber, mGradeNumber);

        array.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 高度 = 图片的高度 + padding
        int height = mNormalBitmap.getHeight();
        height = getPaddingBottom() + getPaddingTop() + height;
        // 宽度 = 图片的宽度 * 数量 + padding
        int width = mNormalBitmap.getWidth() * mGradeNumber;
        width = (getPaddingLeft() + getPaddingRight()) * mGradeNumber + width;
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // super.onDraw(canvas);
        // 使用for循环,画mGradeNumber张图
        int halfGradeNumber = mCurrentGradeNumber % 2;   // 是否需要画半个
        int gradeNumber = mCurrentGradeNumber / 2;      // 需要画几个整的
        for (int i = 1; i <= mGradeNumber; i++) {
            Bitmap tempBitmap = mNormalBitmap;
            if (i <= gradeNumber) {
                tempBitmap = mSelectBitmap;
            }
            if (i == (gradeNumber + 1) && halfGradeNumber == 1) {
                tempBitmap = mHalfBitmap;
            }
            int x = getPaddingLeft() + (i - 1) * (mNormalBitmap.getWidth() + getPaddingLeft() + getPaddingRight());
            canvas.drawBitmap(tempBitmap, x, 0, null);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int temGradeNumber = mCurrentGradeNumber;
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                // 判断当前手指的位置,根据位置计算出分数
                float moveX = event.getX();   // getX:获取当前控件的位置,getRawX:获取屏幕的x的位置
                // 这里只算半个星星,所以只算一半的padding
                mCurrentGradeNumber = (int) (moveX / (mHalfBitmap.getWidth() / 2 + getPaddingLeft())) + 1;
                if (mCurrentGradeNumber <= 0) mCurrentGradeNumber = 0;
                if (mCurrentGradeNumber >= 2 * mGradeNumber) mCurrentGradeNumber = 2 * mGradeNumber;

                // 需要尽量减少onDraw的调用,防止不断地绘制
                if (temGradeNumber != mCurrentGradeNumber) {
                    invalidate();// 刷新
                }
        }
        // return super.onTouchEvent(event);  // 默认是false,代表不消费,down之后的事件都不处理
        return true;  // 这里需要配置为true,不然move会失效
    }
}

在这里插入图片描述

2.2.4 修改activity_main.xml

<!-- /View6/app/src/main/res/layout/activity_main.xml -->
<?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">

    <TextView
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点评助手!" />

    <RatingBar
        android:layout_gravity="center"
        android:layout_width="240dp"
        android:layout_height="wrap_content"/>

    <com.example.view6.RatingBar
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        app:gradeNumber="5"
        app:starNormal="@drawable/no_star"
        app:starHalf="@drawable/half_star"
        app:starSelect="@drawable/yes_star" />

</LinearLayout>

3. 触摸交换与内存优化

触摸交互:分别对down、move、up监听,并根据手指位置分别画选中一半和全选的。
内存优化:发现其实只监听move就行了,且只在手指从一半星星移动到另一半星星时绘制,减少不必要的消耗。

4. onTouch()源码分析

  • 为什么return super.onTouchEvent(event); 只走down,而return true;就可以down、move、up都能执行?

    • 为什么return super.onTouchEvent(event);相当于return false?
    //View6/app/src/main/java/com/example/view6/RatingBar.java
            // 执行down时源码分析
            case MotionEvent.ACTION_DOWN:
         return super.onTouchEvent(event);
    -------->
    //Android/Sdk/sources/android-33/android/view/ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
            if (!canceled && !intercepted) {
                    if (newTouchTarget == null && childrenCount != 0) {
                        // 遍历所有的child
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            // 这里的child不为空,所以就调到child即View的方法
            handled = child.dispatchTouchEvent(transformedEvent);
    -------->
    //Android/Sdk/sources/android-33/android/view/View.java
    public boolean dispatchTouchEvent(MotionEvent event) {
            // 由于我们复写了onTouchEvent,所以这里的onTouchEvent(event) 为false
            if (!result && onTouchEvent(event)false) {
                result = true;  // 所以这里就没法赋值
            }
        return result;   // 则这里返回的是false
    
    • 为什么return false;move就不会生效?
    // 接着上面return false的代码继续分析:
    //Android/Sdk/sources/android-33/android/view/ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);  // 执行down的时候会把mFirstTouchTarget清空
    
            if (!canceled && !intercepted) {
                    if (newTouchTarget == null && childrenCount != 0) {
                        // 遍历所有的child
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            // dispatchTransformedTouchEvent返回false
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);  // 该方法就不会执行
    
    private void cancelAndClearTouchTargets(MotionEvent event) {
            clearTouchTargets();  
    
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
    
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        mFirstTouchTarget = target;   // mFirstTouchTarget 在此处赋值,但是没有走,所以mFirstTouchTarget为空
    -------->
    //View6/app/src/main/java/com/example/view6/RatingBar.java
            case MotionEvent.ACTION_DOWN:
            // 执行move时源码分析,已知mFirstTouchTarget为null:
            case MotionEvent.ACTION_MOCE:
         return super.onTouchEvent(event);
    -------->
    //Android/Sdk/sources/android-33/android/view/ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
            // actionMasked为move且mFirstTouchTarget为空,所以这个if不会执行
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
            } else {
                intercepted = true;   // 将走这里,也就是执行拦截
            }
            // 则该if不会走
            if (!canceled && !intercepted) {
                // 则以下方法都不会走,那么执行view的onTouchEvent都不会执行,则move和up都没效果
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    
    • 为何return true;就可以down、move、up都能执行?

    执行true,则newTouchTarget = addTouchTarget(child, idBitsToAssign);就会被执行,mFirstTouchTarget就会有值, if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)为true,intercepted为fasle,if (!canceled && !intercepted) {以及View的onTouchEvent都会被执行。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AndroidRatingBar 控件是一种用户评分控件,可以在应用中显示一组星形图标,用户可以通过点击星形图标来为应用或产品进行评分RatingBar 控件通常用于应用市场或评估应用的功能中。 RatingBar 控件有以下属性: - android:numStars:指定 RatingBar 中星形图标的数量。 - android:rating:指定 RatingBar 的当前评分值。 - android:stepSize:指定评分的步长,例如设置为 0.5,则评分只能是整数或半数(例如 3.0、3.5、4.0 等)。 - android:isIndicator:指定 RatingBar 是否是只读的,即用户是否可以更改评分值。 RatingBar 控件还可以通过监听器来检测评分值的变化。例如,可以使用 OnRatingBarChangeListener 监听器来在评分值发生更改时执行自定义操作。 以下是 RatingBar 控件的示例代码: ``` <RatingBar android:id="@+id/ratingBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:numStars="5" android:stepSize="0.5" android:rating="3.0" android:isIndicator="false" /> ``` 在代码中,可以使用 setRating() 方法来动态设置评分值,例如: ``` RatingBar ratingBar = (RatingBar) findViewById(R.id.ratingBar); ratingBar.setRating(4.5f); ``` 在监听器中,可以使用 getRating() 方法来获取当前的评分值,例如: ``` RatingBar ratingBar = (RatingBar) findViewById(R.id.ratingBar); ratingBar.setOnRatingBarChangeListener(new OnRatingBarChangeListener() { @Override public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { Log.d("MyApp", "Rating changed to " + rating); } }); ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值