1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.compat.annotation.UnsupportedAppUsage;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.drawable.shapes.RectShape;
23 import android.graphics.drawable.shapes.Shape;
24 import android.util.AttributeSet;
25 import android.view.accessibility.AccessibilityNodeInfo;
26 import android.view.inspector.InspectableProperty;
27 
28 import com.android.internal.R;
29 
30 /**
31  * A RatingBar is an extension of SeekBar and ProgressBar that shows a rating in
32  * stars. The user can touch/drag or use arrow keys to set the rating when using
33  * the default size RatingBar. The smaller RatingBar style (
34  * {@link android.R.attr#ratingBarStyleSmall}) and the larger indicator-only
35  * style ({@link android.R.attr#ratingBarStyleIndicator}) do not support user
36  * interaction and should only be used as indicators.
37  * <p>
38  * When using a RatingBar that supports user interaction, placing widgets to the
39  * left or right of the RatingBar is discouraged.
40  * <p>
41  * The number of stars set (via {@link #setNumStars(int)} or in an XML layout)
42  * will be shown when the layout width is set to wrap content (if another layout
43  * width is set, the results may be unpredictable).
44  * <p>
45  * The secondary progress should not be modified by the client as it is used
46  * internally as the background for a fractionally filled star.
47  *
48  * @attr ref android.R.styleable#RatingBar_numStars
49  * @attr ref android.R.styleable#RatingBar_rating
50  * @attr ref android.R.styleable#RatingBar_stepSize
51  * @attr ref android.R.styleable#RatingBar_isIndicator
52  */
53 public class RatingBar extends AbsSeekBar {
54 
55     /**
56      * A callback that notifies clients when the rating has been changed. This
57      * includes changes that were initiated by the user through a touch gesture
58      * or arrow key/trackball as well as changes that were initiated
59      * programmatically.
60      */
61     public interface OnRatingBarChangeListener {
62 
63         /**
64          * Notification that the rating has changed. Clients can use the
65          * fromUser parameter to distinguish user-initiated changes from those
66          * that occurred programmatically. This will not be called continuously
67          * while the user is dragging, only when the user finalizes a rating by
68          * lifting the touch.
69          *
70          * @param ratingBar The RatingBar whose rating has changed.
71          * @param rating The current rating. This will be in the range
72          *            0..numStars.
73          * @param fromUser True if the rating change was initiated by a user's
74          *            touch gesture or arrow key/horizontal trackbell movement.
75          */
onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser)76         void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser);
77 
78     }
79 
80     private int mNumStars = 5;
81 
82     private int mProgressOnStartTracking;
83 
84     @UnsupportedAppUsage
85     private OnRatingBarChangeListener mOnRatingBarChangeListener;
86 
RatingBar(Context context, AttributeSet attrs, int defStyleAttr)87     public RatingBar(Context context, AttributeSet attrs, int defStyleAttr) {
88         this(context, attrs, defStyleAttr, 0);
89     }
90 
RatingBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)91     public RatingBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
92         super(context, attrs, defStyleAttr, defStyleRes);
93 
94         final TypedArray a = context.obtainStyledAttributes(
95                 attrs, R.styleable.RatingBar, defStyleAttr, defStyleRes);
96         saveAttributeDataForStyleable(context, R.styleable.RatingBar,
97                 attrs, a, defStyleAttr, defStyleRes);
98         final int numStars = a.getInt(R.styleable.RatingBar_numStars, mNumStars);
99         setIsIndicator(a.getBoolean(R.styleable.RatingBar_isIndicator, !mIsUserSeekable));
100         final float rating = a.getFloat(R.styleable.RatingBar_rating, -1);
101         final float stepSize = a.getFloat(R.styleable.RatingBar_stepSize, -1);
102         a.recycle();
103 
104         if (numStars > 0 && numStars != mNumStars) {
105             setNumStars(numStars);
106         }
107 
108         if (stepSize >= 0) {
109             setStepSize(stepSize);
110         } else {
111             setStepSize(0.5f);
112         }
113 
114         if (rating >= 0) {
115             setRating(rating);
116         }
117 
118         // A touch inside a star fill up to that fractional area (slightly more
119         // than 0.5 so boundaries round up).
120         mTouchProgressOffset = 0.6f;
121     }
122 
RatingBar(Context context, AttributeSet attrs)123     public RatingBar(Context context, AttributeSet attrs) {
124         this(context, attrs, com.android.internal.R.attr.ratingBarStyle);
125     }
126 
RatingBar(Context context)127     public RatingBar(Context context) {
128         this(context, null);
129     }
130 
131     /**
132      * Sets the listener to be called when the rating changes.
133      *
134      * @param listener The listener.
135      */
setOnRatingBarChangeListener(OnRatingBarChangeListener listener)136     public void setOnRatingBarChangeListener(OnRatingBarChangeListener listener) {
137         mOnRatingBarChangeListener = listener;
138     }
139 
140     /**
141      * @return The listener (may be null) that is listening for rating change
142      *         events.
143      */
getOnRatingBarChangeListener()144     public OnRatingBarChangeListener getOnRatingBarChangeListener() {
145         return mOnRatingBarChangeListener;
146     }
147 
148     /**
149      * Whether this rating bar should only be an indicator (thus non-changeable
150      * by the user).
151      *
152      * @param isIndicator Whether it should be an indicator.
153      *
154      * @attr ref android.R.styleable#RatingBar_isIndicator
155      */
setIsIndicator(boolean isIndicator)156     public void setIsIndicator(boolean isIndicator) {
157         mIsUserSeekable = !isIndicator;
158         if (isIndicator) {
159             setFocusable(FOCUSABLE_AUTO);
160         } else {
161             setFocusable(FOCUSABLE);
162         }
163     }
164 
165     /**
166      * @return Whether this rating bar is only an indicator.
167      *
168      * @attr ref android.R.styleable#RatingBar_isIndicator
169      */
170     @InspectableProperty(name = "isIndicator")
isIndicator()171     public boolean isIndicator() {
172         return !mIsUserSeekable;
173     }
174 
175     /**
176      * Sets the number of stars to show. In order for these to be shown
177      * properly, it is recommended the layout width of this widget be wrap
178      * content.
179      *
180      * @param numStars The number of stars.
181      */
setNumStars(final int numStars)182     public void setNumStars(final int numStars) {
183         if (numStars <= 0) {
184             return;
185         }
186 
187         mNumStars = numStars;
188 
189         // This causes the width to change, so re-layout
190         requestLayout();
191     }
192 
193     /**
194      * Returns the number of stars shown.
195      * @return The number of stars shown.
196      */
197     @InspectableProperty
getNumStars()198     public int getNumStars() {
199         return mNumStars;
200     }
201 
202     /**
203      * Sets the rating (the number of stars filled).
204      *
205      * @param rating The rating to set.
206      */
setRating(float rating)207     public void setRating(float rating) {
208         setProgress(Math.round(rating * getProgressPerStar()));
209     }
210 
211     /**
212      * Gets the current rating (number of stars filled).
213      *
214      * @return The current rating.
215      */
216     @InspectableProperty
getRating()217     public float getRating() {
218         return getProgress() / getProgressPerStar();
219     }
220 
221     /**
222      * Sets the step size (granularity) of this rating bar.
223      *
224      * @param stepSize The step size of this rating bar. For example, if
225      *            half-star granularity is wanted, this would be 0.5.
226      */
setStepSize(float stepSize)227     public void setStepSize(float stepSize) {
228         if (stepSize <= 0) {
229             return;
230         }
231 
232         final float newMax = mNumStars / stepSize;
233         final int newProgress = (int) (newMax / getMax() * getProgress());
234         setMax((int) newMax);
235         setProgress(newProgress);
236     }
237 
238     /**
239      * Gets the step size of this rating bar.
240      *
241      * @return The step size.
242      */
243     @InspectableProperty
getStepSize()244     public float getStepSize() {
245         return (float) getNumStars() / getMax();
246     }
247 
248     /**
249      * @return The amount of progress that fits into a star
250      */
getProgressPerStar()251     private float getProgressPerStar() {
252         if (mNumStars > 0) {
253             return 1f * getMax() / mNumStars;
254         } else {
255             return 1;
256         }
257     }
258 
259     @Override
getDrawableShape()260     Shape getDrawableShape() {
261         // TODO: Once ProgressBar's TODOs are fixed, this won't be needed
262         return new RectShape();
263     }
264 
265     @Override
onProgressRefresh(float scale, boolean fromUser, int progress)266     void onProgressRefresh(float scale, boolean fromUser, int progress) {
267         super.onProgressRefresh(scale, fromUser, progress);
268 
269         // Keep secondary progress in sync with primary
270         updateSecondaryProgress(progress);
271 
272         if (!fromUser) {
273             // Callback for non-user rating changes
274             dispatchRatingChange(false);
275         }
276     }
277 
278     /**
279      * The secondary progress is used to differentiate the background of a
280      * partially filled star. This method keeps the secondary progress in sync
281      * with the progress.
282      *
283      * @param progress The primary progress level.
284      */
updateSecondaryProgress(int progress)285     private void updateSecondaryProgress(int progress) {
286         final float ratio = getProgressPerStar();
287         if (ratio > 0) {
288             final float progressInStars = progress / ratio;
289             final int secondaryProgress = (int) (Math.ceil(progressInStars) * ratio);
290             setSecondaryProgress(secondaryProgress);
291         }
292     }
293 
294     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)295     protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
296         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
297 
298         if (mSampleWidth > 0) {
299             final int width = mSampleWidth * mNumStars;
300             setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, 0),
301                     getMeasuredHeight());
302         }
303     }
304 
305     @Override
onStartTrackingTouch()306     void onStartTrackingTouch() {
307         mProgressOnStartTracking = getProgress();
308 
309         super.onStartTrackingTouch();
310     }
311 
312     @Override
onStopTrackingTouch()313     void onStopTrackingTouch() {
314         super.onStopTrackingTouch();
315 
316         if (getProgress() != mProgressOnStartTracking) {
317             dispatchRatingChange(true);
318         }
319     }
320 
321     @Override
onKeyChange()322     void onKeyChange() {
323         super.onKeyChange();
324         dispatchRatingChange(true);
325     }
326 
dispatchRatingChange(boolean fromUser)327     void dispatchRatingChange(boolean fromUser) {
328         if (mOnRatingBarChangeListener != null) {
329             mOnRatingBarChangeListener.onRatingChanged(this, getRating(),
330                     fromUser);
331         }
332     }
333 
334     @Override
setMax(int max)335     public synchronized void setMax(int max) {
336         // Disallow max progress = 0
337         if (max <= 0) {
338             return;
339         }
340 
341         super.setMax(max);
342     }
343 
344     @Override
getAccessibilityClassName()345     public CharSequence getAccessibilityClassName() {
346         return RatingBar.class.getName();
347     }
348 
349     /** @hide */
350     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)351     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
352         super.onInitializeAccessibilityNodeInfoInternal(info);
353 
354         if (canUserSetProgress()) {
355             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS);
356         }
357     }
358 
359     @Override
canUserSetProgress()360     boolean canUserSetProgress() {
361         return super.canUserSetProgress() && !isIndicator();
362     }
363 }
364