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 OnStateChangeListener mOnStatePreChangeListener;
     68     private int mState = UNSET;
     69     private int[] mImageIds;
     70     private int[] mDescIds;
     71     private int mLevel;
     72     private boolean mClickEnabled = true;
     73     private int mParentSize;
     74     private int mAnimDirection;
     75     private Matrix mMatrix = new Matrix();
     76     private ValueAnimator mAnimator;
     77 
     78     public MultiToggleImageButton(Context context) {
     79         super(context);
     80         init();
     81     }
     82 
     83     public MultiToggleImageButton(Context context, AttributeSet attrs) {
     84         super(context, attrs);
     85         init();
     86         parseAttributes(context, attrs);
     87         setState(0);
     88     }
     89 
     90     public MultiToggleImageButton(Context context, AttributeSet attrs, int defStyle) {
     91         super(context, attrs, defStyle);
     92         init();
     93         parseAttributes(context, attrs);
     94         setState(0);
     95     }
     96 
     97     /*
     98      * Set the state change listener.
     99      *
    100      * @param onStateChangeListener The listener to set.
    101      */
    102     public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) {
    103         mOnStateChangeListener = onStateChangeListener;
    104     }
    105 
    106     /**
    107      * Set the listener that will be invoked right after the click event before
    108      * all the operations required to change the state of the button.  This
    109      * listener is useful if the client doesn't want to wait until the state
    110      * change is completed to perform certain tasks.
    111      *
    112      * @param onStatePreChangeListener The listener to set.
    113      */
    114     public void setOnPreChangeListener(OnStateChangeListener onStatePreChangeListener) {
    115         mOnStatePreChangeListener = onStatePreChangeListener;
    116     }
    117 
    118     /*
    119      * Get the current button state.
    120      *
    121      */
    122     public int getState() {
    123         return mState;
    124     }
    125 
    126     /*
    127      * Set the current button state, thus causing the state change listener to
    128      * get called.
    129      *
    130      * @param state the desired state
    131      */
    132     public void setState(int state) {
    133         setState(state, true);
    134     }
    135 
    136     /*
    137      * Set the current button state.
    138      *
    139      * @param state the desired state
    140      * @param callListener should the state change listener be called?
    141      */
    142     public void setState(final int state, final boolean callListener) {
    143         setStateAnimatedInternal(state, callListener);
    144     }
    145 
    146     /**
    147      * Set the current button state via an animated transition.
    148      *
    149      * @param state
    150      * @param callListener
    151      */
    152     private void setStateAnimatedInternal(final int state, final boolean callListener) {
    153         if(callListener && mOnStatePreChangeListener != null) {
    154             mOnStatePreChangeListener.stateChanged(MultiToggleImageButton.this, mState);
    155         }
    156 
    157         if (mState == state || mState == UNSET) {
    158             setStateInternal(state, callListener);
    159             return;
    160         }
    161 
    162         if (mImageIds == null) {
    163             return;
    164         }
    165 
    166         new AsyncTask<Integer, Void, Bitmap>() {
    167             @Override
    168             protected Bitmap doInBackground(Integer... params) {
    169                 return combine(params[0], params[1]);
    170             }
    171 
    172             @Override
    173             protected void onPostExecute(Bitmap bitmap) {
    174                 if (bitmap == null) {
    175                     setStateInternal(state, callListener);
    176                 } else {
    177                     setImageBitmap(bitmap);
    178 
    179                     int offset;
    180                     if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
    181                         offset = (mParentSize+getHeight())/2;
    182                     } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
    183                         offset = (mParentSize+getWidth())/2;
    184                     } else {
    185                         return;
    186                     }
    187 
    188                     mAnimator.setFloatValues(-offset, 0.0f);
    189                     AnimatorSet s = new AnimatorSet();
    190                     s.play(mAnimator);
    191                     s.addListener(new AnimatorListenerAdapter() {
    192                         @Override
    193                         public void onAnimationStart(Animator animation) {
    194                             setClickEnabled(false);
    195                         }
    196 
    197                         @Override
    198                         public void onAnimationEnd(Animator animation) {
    199                             setStateInternal(state, callListener);
    200                             setClickEnabled(true);
    201                         }
    202                     });
    203                     s.start();
    204                 }
    205             }
    206         }.execute(mState, state);
    207     }
    208 
    209     /**
    210      * Enable or disable click reactions for this button
    211      * without affecting visual state.
    212      * For most cases you'll want to use {@link #setEnabled(boolean)}.
    213      * @param enabled True if click enabled, false otherwise.
    214      */
    215     public void setClickEnabled(boolean enabled) {
    216         mClickEnabled = enabled;
    217     }
    218 
    219     private void setStateInternal(int state, boolean callListener) {
    220         mState = state;
    221         if (mImageIds != null) {
    222             setImageByState(mState);
    223         }
    224 
    225         if (mDescIds != null) {
    226             String oldContentDescription = String.valueOf(getContentDescription());
    227             String newContentDescription = getResources().getString(mDescIds[mState]);
    228             if (oldContentDescription != null && !oldContentDescription.isEmpty()
    229                     && !oldContentDescription.equals(newContentDescription)) {
    230                 setContentDescription(newContentDescription);
    231                 String announceChange = getResources().getString(
    232                     R.string.button_change_announcement, newContentDescription);
    233                 announceForAccessibility(announceChange);
    234             }
    235         }
    236         super.setImageLevel(mLevel);
    237 
    238         if (callListener && mOnStateChangeListener != null) {
    239             mOnStateChangeListener.stateChanged(MultiToggleImageButton.this, getState());
    240         }
    241     }
    242 
    243     private void nextState() {
    244         int state = mState + 1;
    245         if (state >= mImageIds.length) {
    246             state = 0;
    247         }
    248         setState(state);
    249     }
    250 
    251     protected void init() {
    252         this.setOnClickListener(new View.OnClickListener() {
    253             @Override
    254             public void onClick(View v) {
    255                 if (mClickEnabled) {
    256                     nextState();
    257                 }
    258             }
    259         });
    260         setScaleType(ImageView.ScaleType.MATRIX);
    261 
    262         mAnimator = ValueAnimator.ofFloat(0.0f, 0.0f);
    263         mAnimator.setDuration(ANIM_DURATION_MS);
    264         mAnimator.setInterpolator(Gusterpolator.INSTANCE);
    265         mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    266             @Override
    267             public void onAnimationUpdate(ValueAnimator animation) {
    268                 mMatrix.reset();
    269                 if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
    270                     mMatrix.setTranslate(0.0f, (Float) animation.getAnimatedValue());
    271                 } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
    272                     mMatrix.setTranslate((Float) animation.getAnimatedValue(), 0.0f);
    273                 }
    274 
    275                 setImageMatrix(mMatrix);
    276                 invalidate();
    277             }
    278         });
    279     }
    280 
    281     private void parseAttributes(Context context, AttributeSet attrs) {
    282         TypedArray a = context.getTheme().obtainStyledAttributes(
    283             attrs,
    284             R.styleable.MultiToggleImageButton,
    285             0, 0);
    286         int imageIds = a.getResourceId(R.styleable.MultiToggleImageButton_imageIds, 0);
    287         if (imageIds > 0) {
    288             overrideImageIds(imageIds);
    289         }
    290         int descIds = a.getResourceId(R.styleable.MultiToggleImageButton_contentDescriptionIds, 0);
    291         if (descIds > 0) {
    292             overrideContentDescriptions(descIds);
    293         }
    294         a.recycle();
    295     }
    296 
    297     /**
    298      * Override the image ids of this button.
    299      */
    300     public void overrideImageIds(int resId) {
    301         TypedArray ids = null;
    302         try {
    303             ids = getResources().obtainTypedArray(resId);
    304             mImageIds = new int[ids.length()];
    305             for (int i = 0; i < ids.length(); i++) {
    306                 mImageIds[i] = ids.getResourceId(i, 0);
    307             }
    308         } finally {
    309             if (ids != null) {
    310                 ids.recycle();
    311             }
    312         }
    313 
    314         if (mState >= 0 && mState < mImageIds.length) {
    315             setImageByState(mState);
    316         }
    317     }
    318 
    319     /**
    320      * Override the content descriptions of this button.
    321      */
    322     public void overrideContentDescriptions(int resId) {
    323         TypedArray ids = null;
    324         try {
    325             ids = getResources().obtainTypedArray(resId);
    326             mDescIds = new int[ids.length()];
    327             for (int i = 0; i < ids.length(); i++) {
    328                 mDescIds[i] = ids.getResourceId(i, 0);
    329             }
    330         } finally {
    331             if (ids != null) {
    332                 ids.recycle();
    333             }
    334         }
    335     }
    336 
    337     /**
    338      * Set size info (either width or height, as necessary) of the view containing
    339      * this button. Used for offset calculations during animation.
    340      * @param s The size.
    341      */
    342     public void setParentSize(int s) {
    343         mParentSize = s;
    344     }
    345 
    346     /**
    347      * Set the animation direction.
    348      * @param d Either ANIM_DIRECTION_VERTICAL or ANIM_DIRECTION_HORIZONTAL.
    349      */
    350     public void setAnimDirection(int d) {
    351         mAnimDirection = d;
    352     }
    353 
    354     @Override
    355     public void setImageLevel(int level) {
    356         super.setImageLevel(level);
    357         mLevel = level;
    358     }
    359 
    360     private void setImageByState(int state) {
    361         if (mImageIds != null) {
    362             setImageResource(mImageIds[state]);
    363         }
    364         super.setImageLevel(mLevel);
    365     }
    366 
    367     private Bitmap combine(int oldState, int newState) {
    368         // In some cases, a new set of image Ids are set via overrideImageIds()
    369         // and oldState or newState overrun the array.
    370         // check here for that.
    371         if (oldState >= mImageIds.length || newState >= mImageIds.length) {
    372             return null;
    373         }
    374 
    375         int width = getWidth();
    376         int height = getHeight();
    377 
    378         if (width <= 0 || height <= 0) {
    379             return null;
    380         }
    381 
    382         int[] enabledState = new int[] {android.R.attr.state_enabled};
    383 
    384         // new state
    385         Drawable newDrawable = getResources().getDrawable(mImageIds[newState]).mutate();
    386         newDrawable.setState(enabledState);
    387 
    388         // old state
    389         Drawable oldDrawable = getResources().getDrawable(mImageIds[oldState]).mutate();
    390         oldDrawable.setState(enabledState);
    391 
    392         // combine 'em
    393         Bitmap bitmap = null;
    394         if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
    395             int bitmapHeight = (height*2) + ((mParentSize - height)/2);
    396             int oldBitmapOffset = height + ((mParentSize - height)/2);
    397             bitmap = Bitmap.createBitmap(width, bitmapHeight, Bitmap.Config.ARGB_8888);
    398             Canvas canvas = new Canvas(bitmap);
    399             newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
    400             oldDrawable.setBounds(0, oldBitmapOffset, oldDrawable.getIntrinsicWidth(), oldDrawable.getIntrinsicHeight()+oldBitmapOffset);
    401             newDrawable.draw(canvas);
    402             oldDrawable.draw(canvas);
    403         } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
    404             int bitmapWidth = (width*2) + ((mParentSize - width)/2);
    405             int oldBitmapOffset = width + ((mParentSize - width)/2);
    406             bitmap = Bitmap.createBitmap(bitmapWidth, height, Bitmap.Config.ARGB_8888);
    407             Canvas canvas = new Canvas(bitmap);
    408             newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
    409             oldDrawable.setBounds(oldBitmapOffset, 0, oldDrawable.getIntrinsicWidth()+oldBitmapOffset, oldDrawable.getIntrinsicHeight());
    410             newDrawable.draw(canvas);
    411             oldDrawable.draw(canvas);
    412         }
    413 
    414         return bitmap;
    415     }
    416 }