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          */
stateChanged(View view, int state)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 
MultiToggleImageButton(Context context)78     public MultiToggleImageButton(Context context) {
79         super(context);
80         init();
81     }
82 
MultiToggleImageButton(Context context, AttributeSet attrs)83     public MultiToggleImageButton(Context context, AttributeSet attrs) {
84         super(context, attrs);
85         init();
86         parseAttributes(context, attrs);
87         setState(0);
88     }
89 
MultiToggleImageButton(Context context, AttributeSet attrs, int defStyle)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      */
setOnStateChangeListener(OnStateChangeListener onStateChangeListener)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      */
setOnPreChangeListener(OnStateChangeListener onStatePreChangeListener)114     public void setOnPreChangeListener(OnStateChangeListener onStatePreChangeListener) {
115         mOnStatePreChangeListener = onStatePreChangeListener;
116     }
117 
118     /*
119      * Get the current button state.
120      *
121      */
getState()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      */
setState(int state)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      */
setState(final int state, final boolean callListener)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      */
setStateAnimatedInternal(final int state, final boolean callListener)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      */
setClickEnabled(boolean enabled)215     public void setClickEnabled(boolean enabled) {
216         mClickEnabled = enabled;
217     }
218 
setStateInternal(int state, boolean callListener)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 
nextState()243     private void nextState() {
244         int state = mState + 1;
245         if (state >= mImageIds.length) {
246             state = 0;
247         }
248         setState(state);
249     }
250 
init()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 
parseAttributes(Context context, AttributeSet attrs)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      */
overrideImageIds(int resId)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      */
overrideContentDescriptions(int resId)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      */
setParentSize(int s)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      */
setAnimDirection(int d)350     public void setAnimDirection(int d) {
351         mAnimDirection = d;
352     }
353 
354     @Override
setImageLevel(int level)355     public void setImageLevel(int level) {
356         super.setImageLevel(level);
357         mLevel = level;
358     }
359 
setImageByState(int state)360     private void setImageByState(int state) {
361         if (mImageIds != null) {
362             setImageResource(mImageIds[state]);
363         }
364         super.setImageLevel(mLevel);
365     }
366 
combine(int oldState, int newState)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 }