Home | History | Annotate | Download | only in camera
      1 /*
      2  * Copyright (C) 2013 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 com.android.camera;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.AnimatorSet;
     22 import android.animation.ValueAnimator;
     23 import android.content.Context;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Bitmap;
     26 import android.graphics.Canvas;
     27 import android.graphics.Matrix;
     28 import android.graphics.drawable.Drawable;
     29 import android.os.AsyncTask;
     30 import android.util.AttributeSet;
     31 import android.view.View;
     32 import android.widget.ImageButton;
     33 import android.widget.ImageView;
     34 
     35 import com.android.camera.util.Gusterpolator;
     36 import com.android.camera2.R;
     37 
     38 /*
     39  * A toggle button that supports two or more states with images rendererd on top
     40  * for each state.
     41  * The button is initialized in an XML layout file with an array reference of
     42  * image ids (e.g. imageIds="@array/camera_flashmode_icons").
     43  * Each image in the referenced array represents a single integer state.
     44  * Every time the user touches the button it gets set to next state in line,
     45  * with the corresponding image drawn onto the face of the button.
     46  * State wraps back to 0 on user touch when button is already at n-1 state.
     47  */
     48 public class MultiToggleImageButton extends ImageButton {
     49     /*
     50      * Listener interface for button state changes.
     51      */
     52     public interface OnStateChangeListener {
     53         /*
     54          * @param view the MultiToggleImageButton that received the touch event
     55          * @param state the new state the button is in
     56          */
     57         public abstract void stateChanged(View view, int state);
     58     }
     59 
     60     public static final int ANIM_DIRECTION_VERTICAL = 0;
     61     public static final int ANIM_DIRECTION_HORIZONTAL = 1;
     62 
     63     private static final int ANIM_DURATION_MS = 250;
     64     private static final int UNSET = -1;
     65 
     66     private OnStateChangeListener mOnStateChangeListener;
     67     private int mState = UNSET;
     68     private int[] mImageIds;
     69     private int[] mDescIds;
     70     private int mLevel;
     71     private boolean mClickEnabled = true;
     72     private int mParentSize;
     73     private int mAnimDirection;
     74     private Matrix mMatrix = new Matrix();
     75     private ValueAnimator mAnimator;
     76 
     77     public MultiToggleImageButton(Context context) {
     78         super(context);
     79         init();
     80     }
     81 
     82     public MultiToggleImageButton(Context context, AttributeSet attrs) {
     83         super(context, attrs);
     84         init();
     85         parseAttributes(context, attrs);
     86         setState(0);
     87     }
     88 
     89     public MultiToggleImageButton(Context context, AttributeSet attrs, int defStyle) {
     90         super(context, attrs, defStyle);
     91         init();
     92         parseAttributes(context, attrs);
     93         setState(0);
     94     }
     95 
     96     /*
     97      * Set the state change listener.
     98      *
     99      * @param onStateChangeListener the listener to set
    100      */
    101     public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) {
    102         mOnStateChangeListener = onStateChangeListener;
    103     }
    104 
    105     /*
    106      * Get the current button state.
    107      *
    108      */
    109     public int getState() {
    110         return mState;
    111     }
    112 
    113     /*
    114      * Set the current button state, thus causing the state change listener to
    115      * get called.
    116      *
    117      * @param state the desired state
    118      */
    119     public void setState(int state) {
    120         setState(state, true);
    121     }
    122 
    123     /*
    124      * Set the current button state.
    125      *
    126      * @param state the desired state
    127      * @param callListener should the state change listener be called?
    128      */
    129     public void setState(final int state, final boolean callListener) {
    130         setStateAnimatedInternal(state, callListener);
    131     }
    132 
    133     /**
    134      * Set the current button state via an animated transition.
    135      *
    136      * @param state
    137      * @param callListener
    138      */
    139     private void setStateAnimatedInternal(final int state, final boolean callListener) {
    140         if (mState == state || mState == UNSET) {
    141             setStateInternal(state, callListener);
    142             return;
    143         }
    144 
    145         if (mImageIds == null) {
    146             return;
    147         }
    148 
    149         new AsyncTask<Integer, Void, Bitmap>() {
    150             @Override
    151             protected Bitmap doInBackground(Integer... params) {
    152                 return combine(params[0], params[1]);
    153             }
    154 
    155             @Override
    156             protected void onPostExecute(Bitmap bitmap) {
    157                 if (bitmap == null) {
    158                     setStateInternal(state, callListener);
    159                 } else {
    160                     setImageBitmap(bitmap);
    161 
    162                     int offset;
    163                     if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
    164                         offset = (mParentSize+getHeight())/2;
    165                     } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
    166                         offset = (mParentSize+getWidth())/2;
    167                     } else {
    168                         return;
    169                     }
    170 
    171                     mAnimator.setFloatValues(-offset, 0.0f);
    172                     AnimatorSet s = new AnimatorSet();
    173                     s.play(mAnimator);
    174                     s.addListener(new AnimatorListenerAdapter() {
    175                         @Override
    176                         public void onAnimationStart(Animator animation) {
    177                             setClickEnabled(false);
    178                         }
    179 
    180                         @Override
    181                         public void onAnimationEnd(Animator animation) {
    182                             setStateInternal(state, callListener);
    183                             setClickEnabled(true);
    184                         }
    185                     });
    186                     s.start();
    187                 }
    188             }
    189         }.execute(mState, state);
    190     }
    191 
    192     /**
    193      * Enable or disable click reactions for this button
    194      * without affecting visual state.
    195      * For most cases you'll want to use {@link #setEnabled(boolean)}.
    196      * @param enabled True if click enabled, false otherwise.
    197      */
    198     public void setClickEnabled(boolean enabled) {
    199         mClickEnabled = enabled;
    200     }
    201 
    202     private void setStateInternal(int state, boolean callListener) {
    203         mState = state;
    204         if (mImageIds != null) {
    205             setImageByState(mState);
    206         }
    207 
    208         if (mDescIds != null) {
    209             String oldContentDescription = String.valueOf(getContentDescription());
    210             String newContentDescription = getResources().getString(mDescIds[mState]);
    211             if (oldContentDescription != null && !oldContentDescription.isEmpty()
    212                     && !oldContentDescription.equals(newContentDescription)) {
    213                 setContentDescription(newContentDescription);
    214                 String announceChange = getResources().getString(
    215                     R.string.button_change_announcement, newContentDescription);
    216                 announceForAccessibility(announceChange);
    217             }
    218         }
    219         super.setImageLevel(mLevel);
    220 
    221         if (callListener && mOnStateChangeListener != null) {
    222             mOnStateChangeListener.stateChanged(MultiToggleImageButton.this, getState());
    223         }
    224     }
    225 
    226     private void nextState() {
    227         int state = mState + 1;
    228         if (state >= mImageIds.length) {
    229             state = 0;
    230         }
    231         setState(state);
    232     }
    233 
    234     protected void init() {
    235         this.setOnClickListener(new View.OnClickListener() {
    236             @Override
    237             public void onClick(View v) {
    238                 if (mClickEnabled) {
    239                     nextState();
    240                 }
    241             }
    242         });
    243         setScaleType(ImageView.ScaleType.MATRIX);
    244 
    245         mAnimator = ValueAnimator.ofFloat(0.0f, 0.0f);
    246         mAnimator.setDuration(ANIM_DURATION_MS);
    247         mAnimator.setInterpolator(Gusterpolator.INSTANCE);
    248         mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    249             @Override
    250             public void onAnimationUpdate(ValueAnimator animation) {
    251                 mMatrix.reset();
    252                 if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
    253                     mMatrix.setTranslate(0.0f, (Float) animation.getAnimatedValue());
    254                 } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
    255                     mMatrix.setTranslate((Float) animation.getAnimatedValue(), 0.0f);
    256                 }
    257 
    258                 setImageMatrix(mMatrix);
    259                 invalidate();
    260             }
    261         });
    262     }
    263 
    264     private void parseAttributes(Context context, AttributeSet attrs) {
    265         TypedArray a = context.getTheme().obtainStyledAttributes(
    266             attrs,
    267             R.styleable.MultiToggleImageButton,
    268             0, 0);
    269         int imageIds = a.getResourceId(R.styleable.MultiToggleImageButton_imageIds, 0);
    270         if (imageIds > 0) {
    271             overrideImageIds(imageIds);
    272         }
    273         int descIds = a.getResourceId(R.styleable.MultiToggleImageButton_contentDescriptionIds, 0);
    274         if (descIds > 0) {
    275             overrideContentDescriptions(descIds);
    276         }
    277         a.recycle();
    278     }
    279 
    280     /**
    281      * Override the image ids of this button.
    282      */
    283     public void overrideImageIds(int resId) {
    284         TypedArray ids = null;
    285         try {
    286             ids = getResources().obtainTypedArray(resId);
    287             mImageIds = new int[ids.length()];
    288             for (int i = 0; i < ids.length(); i++) {
    289                 mImageIds[i] = ids.getResourceId(i, 0);
    290             }
    291         } finally {
    292             if (ids != null) {
    293                 ids.recycle();
    294             }
    295         }
    296     }
    297 
    298     /**
    299      * Override the content descriptions of this button.
    300      */
    301     public void overrideContentDescriptions(int resId) {
    302         TypedArray ids = null;
    303         try {
    304             ids = getResources().obtainTypedArray(resId);
    305             mDescIds = new int[ids.length()];
    306             for (int i = 0; i < ids.length(); i++) {
    307                 mDescIds[i] = ids.getResourceId(i, 0);
    308             }
    309         } finally {
    310             if (ids != null) {
    311                 ids.recycle();
    312             }
    313         }
    314     }
    315 
    316     /**
    317      * Set size info (either width or height, as necessary) of the view containing
    318      * this button. Used for offset calculations during animation.
    319      * @param s The size.
    320      */
    321     public void setParentSize(int s) {
    322         mParentSize = s;
    323     }
    324 
    325     /**
    326      * Set the animation direction.
    327      * @param d Either ANIM_DIRECTION_VERTICAL or ANIM_DIRECTION_HORIZONTAL.
    328      */
    329     public void setAnimDirection(int d) {
    330         mAnimDirection = d;
    331     }
    332 
    333     @Override
    334     public void setImageLevel(int level) {
    335         super.setImageLevel(level);
    336         mLevel = level;
    337     }
    338 
    339     private void setImageByState(int state) {
    340         if (mImageIds != null) {
    341             setImageResource(mImageIds[state]);
    342         }
    343         super.setImageLevel(mLevel);
    344     }
    345 
    346     private Bitmap combine(int oldState, int newState) {
    347         // in some cases, a new set of image Ids are set via overrideImageIds()
    348         // and oldState overruns the array.
    349         // check here for that.
    350         if (oldState >= mImageIds.length) {
    351             return null;
    352         }
    353 
    354         int width = getWidth();
    355         int height = getHeight();
    356 
    357         if (width <= 0 || height <= 0) {
    358             return null;
    359         }
    360 
    361         int[] enabledState = new int[] {android.R.attr.state_enabled};
    362 
    363         // new state
    364         Drawable newDrawable = getResources().getDrawable(mImageIds[newState]).mutate();
    365         newDrawable.setState(enabledState);
    366 
    367         // old state
    368         Drawable oldDrawable = getResources().getDrawable(mImageIds[oldState]).mutate();
    369         oldDrawable.setState(enabledState);
    370 
    371         // combine 'em
    372         Bitmap bitmap = null;
    373         if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
    374             int bitmapHeight = (height*2) + ((mParentSize - height)/2);
    375             int oldBitmapOffset = height + ((mParentSize - height)/2);
    376             bitmap = Bitmap.createBitmap(width, bitmapHeight, Bitmap.Config.ARGB_8888);
    377             Canvas canvas = new Canvas(bitmap);
    378             newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
    379             oldDrawable.setBounds(0, oldBitmapOffset, oldDrawable.getIntrinsicWidth(), oldDrawable.getIntrinsicHeight()+oldBitmapOffset);
    380             newDrawable.draw(canvas);
    381             oldDrawable.draw(canvas);
    382         } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
    383             int bitmapWidth = (width*2) + ((mParentSize - width)/2);
    384             int oldBitmapOffset = width + ((mParentSize - width)/2);
    385             bitmap = Bitmap.createBitmap(bitmapWidth, height, Bitmap.Config.ARGB_8888);
    386             Canvas canvas = new Canvas(bitmap);
    387             newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
    388             oldDrawable.setBounds(oldBitmapOffset, 0, oldDrawable.getIntrinsicWidth()+oldBitmapOffset, oldDrawable.getIntrinsicHeight());
    389             newDrawable.draw(canvas);
    390             oldDrawable.draw(canvas);
    391         }
    392 
    393         return bitmap;
    394     }
    395 }