Home | History | Annotate | Download | only in drawable
      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.graphics.drawable;
     18 
     19 import com.android.internal.R;
     20 
     21 import org.xmlpull.v1.XmlPullParser;
     22 import org.xmlpull.v1.XmlPullParserException;
     23 
     24 import android.annotation.NonNull;
     25 import android.annotation.Nullable;
     26 import android.graphics.Canvas;
     27 import android.graphics.Rect;
     28 import android.content.res.Resources;
     29 import android.content.res.TypedArray;
     30 import android.content.res.Resources.Theme;
     31 import android.util.MathUtils;
     32 import android.util.TypedValue;
     33 import android.util.AttributeSet;
     34 
     35 import java.io.IOException;
     36 
     37 /**
     38  * <p>
     39  * A Drawable that can rotate another Drawable based on the current level value.
     40  * The start and end angles of rotation can be controlled to map any circular
     41  * arc to the level values range.
     42  * <p>
     43  * It can be defined in an XML file with the <code>&lt;rotate&gt;</code> element.
     44  * For more information, see the guide to
     45  * <a href="{@docRoot}guide/topics/resources/animation-resource.html">Animation Resources</a>.
     46  *
     47  * @attr ref android.R.styleable#RotateDrawable_visible
     48  * @attr ref android.R.styleable#RotateDrawable_fromDegrees
     49  * @attr ref android.R.styleable#RotateDrawable_toDegrees
     50  * @attr ref android.R.styleable#RotateDrawable_pivotX
     51  * @attr ref android.R.styleable#RotateDrawable_pivotY
     52  * @attr ref android.R.styleable#RotateDrawable_drawable
     53  */
     54 public class RotateDrawable extends DrawableWrapper {
     55     private static final int MAX_LEVEL = 10000;
     56 
     57     private RotateState mState;
     58 
     59     /**
     60      * Creates a new rotating drawable with no wrapped drawable.
     61      */
     62     public RotateDrawable() {
     63         this(new RotateState(null, null), null);
     64     }
     65 
     66     @Override
     67     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
     68             @NonNull AttributeSet attrs, @Nullable Theme theme)
     69             throws XmlPullParserException, IOException {
     70         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RotateDrawable);
     71 
     72         // Inflation will advance the XmlPullParser and AttributeSet.
     73         super.inflate(r, parser, attrs, theme);
     74 
     75         updateStateFromTypedArray(a);
     76         verifyRequiredAttributes(a);
     77         a.recycle();
     78     }
     79 
     80     @Override
     81     public void applyTheme(@NonNull Theme t) {
     82         super.applyTheme(t);
     83 
     84         final RotateState state = mState;
     85         if (state == null) {
     86             return;
     87         }
     88 
     89         if (state.mThemeAttrs != null) {
     90             final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.RotateDrawable);
     91             try {
     92                 updateStateFromTypedArray(a);
     93                 verifyRequiredAttributes(a);
     94             } catch (XmlPullParserException e) {
     95                 rethrowAsRuntimeException(e);
     96             } finally {
     97                 a.recycle();
     98             }
     99         }
    100     }
    101 
    102     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
    103         // If we're not waiting on a theme, verify required attributes.
    104         if (getDrawable() == null && (mState.mThemeAttrs == null
    105                 || mState.mThemeAttrs[R.styleable.RotateDrawable_drawable] == 0)) {
    106             throw new XmlPullParserException(a.getPositionDescription()
    107                     + ": <rotate> tag requires a 'drawable' attribute or "
    108                     + "child tag defining a drawable");
    109         }
    110     }
    111 
    112     private void updateStateFromTypedArray(@NonNull TypedArray a) {
    113         final RotateState state = mState;
    114         if (state == null) {
    115             return;
    116         }
    117 
    118         // Account for any configuration changes.
    119         state.mChangingConfigurations |= a.getChangingConfigurations();
    120 
    121         // Extract the theme attributes, if any.
    122         state.mThemeAttrs = a.extractThemeAttrs();
    123 
    124         if (a.hasValue(R.styleable.RotateDrawable_pivotX)) {
    125             final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX);
    126             state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
    127             state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
    128         }
    129 
    130         if (a.hasValue(R.styleable.RotateDrawable_pivotY)) {
    131             final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotY);
    132             state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION;
    133             state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
    134         }
    135 
    136         state.mFromDegrees = a.getFloat(
    137                 R.styleable.RotateDrawable_fromDegrees, state.mFromDegrees);
    138         state.mToDegrees = a.getFloat(
    139                 R.styleable.RotateDrawable_toDegrees, state.mToDegrees);
    140         state.mCurrentDegrees = state.mFromDegrees;
    141     }
    142 
    143     @Override
    144     public void draw(Canvas canvas) {
    145         final Drawable d = getDrawable();
    146         final Rect bounds = d.getBounds();
    147         final int w = bounds.right - bounds.left;
    148         final int h = bounds.bottom - bounds.top;
    149         final RotateState st = mState;
    150         final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX;
    151         final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY;
    152 
    153         final int saveCount = canvas.save();
    154         canvas.rotate(st.mCurrentDegrees, px + bounds.left, py + bounds.top);
    155         d.draw(canvas);
    156         canvas.restoreToCount(saveCount);
    157     }
    158 
    159     /**
    160      * Sets the start angle for rotation.
    161      *
    162      * @param fromDegrees starting angle in degrees
    163      * @see #getFromDegrees()
    164      * @attr ref android.R.styleable#RotateDrawable_fromDegrees
    165      */
    166     public void setFromDegrees(float fromDegrees) {
    167         if (mState.mFromDegrees != fromDegrees) {
    168             mState.mFromDegrees = fromDegrees;
    169             invalidateSelf();
    170         }
    171     }
    172 
    173     /**
    174      * @return starting angle for rotation in degrees
    175      * @see #setFromDegrees(float)
    176      * @attr ref android.R.styleable#RotateDrawable_fromDegrees
    177      */
    178     public float getFromDegrees() {
    179         return mState.mFromDegrees;
    180     }
    181 
    182     /**
    183      * Sets the end angle for rotation.
    184      *
    185      * @param toDegrees ending angle in degrees
    186      * @see #getToDegrees()
    187      * @attr ref android.R.styleable#RotateDrawable_toDegrees
    188      */
    189     public void setToDegrees(float toDegrees) {
    190         if (mState.mToDegrees != toDegrees) {
    191             mState.mToDegrees = toDegrees;
    192             invalidateSelf();
    193         }
    194     }
    195 
    196     /**
    197      * @return ending angle for rotation in degrees
    198      * @see #setToDegrees(float)
    199      * @attr ref android.R.styleable#RotateDrawable_toDegrees
    200      */
    201     public float getToDegrees() {
    202         return mState.mToDegrees;
    203     }
    204 
    205     /**
    206      * Sets the X position around which the drawable is rotated.
    207      * <p>
    208      * If the X pivot is relative (as specified by
    209      * {@link #setPivotXRelative(boolean)}), then the position represents a
    210      * fraction of the drawable width. Otherwise, the position represents an
    211      * absolute value in pixels.
    212      *
    213      * @param pivotX X position around which to rotate
    214      * @see #setPivotXRelative(boolean)
    215      * @attr ref android.R.styleable#RotateDrawable_pivotX
    216      */
    217     public void setPivotX(float pivotX) {
    218         if (mState.mPivotX != pivotX) {
    219             mState.mPivotX = pivotX;
    220             invalidateSelf();
    221         }
    222     }
    223 
    224     /**
    225      * @return X position around which to rotate
    226      * @see #setPivotX(float)
    227      * @attr ref android.R.styleable#RotateDrawable_pivotX
    228      */
    229     public float getPivotX() {
    230         return mState.mPivotX;
    231     }
    232 
    233     /**
    234      * Sets whether the X pivot value represents a fraction of the drawable
    235      * width or an absolute value in pixels.
    236      *
    237      * @param relative true if the X pivot represents a fraction of the drawable
    238      *            width, or false if it represents an absolute value in pixels
    239      * @see #isPivotXRelative()
    240      */
    241     public void setPivotXRelative(boolean relative) {
    242         if (mState.mPivotXRel != relative) {
    243             mState.mPivotXRel = relative;
    244             invalidateSelf();
    245         }
    246     }
    247 
    248     /**
    249      * @return true if the X pivot represents a fraction of the drawable width,
    250      *         or false if it represents an absolute value in pixels
    251      * @see #setPivotXRelative(boolean)
    252      */
    253     public boolean isPivotXRelative() {
    254         return mState.mPivotXRel;
    255     }
    256 
    257     /**
    258      * Sets the Y position around which the drawable is rotated.
    259      * <p>
    260      * If the Y pivot is relative (as specified by
    261      * {@link #setPivotYRelative(boolean)}), then the position represents a
    262      * fraction of the drawable height. Otherwise, the position represents an
    263      * absolute value in pixels.
    264      *
    265      * @param pivotY Y position around which to rotate
    266      * @see #getPivotY()
    267      * @attr ref android.R.styleable#RotateDrawable_pivotY
    268      */
    269     public void setPivotY(float pivotY) {
    270         if (mState.mPivotY != pivotY) {
    271             mState.mPivotY = pivotY;
    272             invalidateSelf();
    273         }
    274     }
    275 
    276     /**
    277      * @return Y position around which to rotate
    278      * @see #setPivotY(float)
    279      * @attr ref android.R.styleable#RotateDrawable_pivotY
    280      */
    281     public float getPivotY() {
    282         return mState.mPivotY;
    283     }
    284 
    285     /**
    286      * Sets whether the Y pivot value represents a fraction of the drawable
    287      * height or an absolute value in pixels.
    288      *
    289      * @param relative True if the Y pivot represents a fraction of the drawable
    290      *            height, or false if it represents an absolute value in pixels
    291      * @see #isPivotYRelative()
    292      */
    293     public void setPivotYRelative(boolean relative) {
    294         if (mState.mPivotYRel != relative) {
    295             mState.mPivotYRel = relative;
    296             invalidateSelf();
    297         }
    298     }
    299 
    300     /**
    301      * @return true if the Y pivot represents a fraction of the drawable height,
    302      *         or false if it represents an absolute value in pixels
    303      * @see #setPivotYRelative(boolean)
    304      */
    305     public boolean isPivotYRelative() {
    306         return mState.mPivotYRel;
    307     }
    308 
    309     @Override
    310     protected boolean onLevelChange(int level) {
    311         super.onLevelChange(level);
    312 
    313         final float value = level / (float) MAX_LEVEL;
    314         final float degrees = MathUtils.lerp(mState.mFromDegrees, mState.mToDegrees, value);
    315         mState.mCurrentDegrees = degrees;
    316 
    317         invalidateSelf();
    318         return true;
    319     }
    320 
    321     @Override
    322     DrawableWrapperState mutateConstantState() {
    323         mState = new RotateState(mState, null);
    324         return mState;
    325     }
    326 
    327     static final class RotateState extends DrawableWrapper.DrawableWrapperState {
    328         private int[] mThemeAttrs;
    329 
    330         boolean mPivotXRel = true;
    331         float mPivotX = 0.5f;
    332         boolean mPivotYRel = true;
    333         float mPivotY = 0.5f;
    334         float mFromDegrees = 0.0f;
    335         float mToDegrees = 360.0f;
    336         float mCurrentDegrees = 0.0f;
    337 
    338         RotateState(RotateState orig, Resources res) {
    339             super(orig, res);
    340 
    341             if (orig != null) {
    342                 mPivotXRel = orig.mPivotXRel;
    343                 mPivotX = orig.mPivotX;
    344                 mPivotYRel = orig.mPivotYRel;
    345                 mPivotY = orig.mPivotY;
    346                 mFromDegrees = orig.mFromDegrees;
    347                 mToDegrees = orig.mToDegrees;
    348                 mCurrentDegrees = orig.mCurrentDegrees;
    349             }
    350         }
    351 
    352         @Override
    353         public Drawable newDrawable(Resources res) {
    354             return new RotateDrawable(this, res);
    355         }
    356     }
    357 
    358     private RotateDrawable(RotateState state, Resources res) {
    359         super(state, res);
    360 
    361         mState = state;
    362     }
    363 }
    364