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都会被执行。 - 为什么