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