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