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