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