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 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 
MultiToggleImageButton(Context context)77     public MultiToggleImageButton(Context context) {
78         super(context);
79         init();
80     }
81 
MultiToggleImageButton(Context context, AttributeSet attrs)82     public MultiToggleImageButton(Context context, AttributeSet attrs) {
83         super(context, attrs);
84         init();
85         parseAttributes(context, attrs);
86         setState(0);
87     }
88 
MultiToggleImageButton(Context context, AttributeSet attrs, int defStyle)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      */
setOnStateChangeListener(OnStateChangeListener onStateChangeListener)101     public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) {
102         mOnStateChangeListener = onStateChangeListener;
103     }
104 
105     /*
106      * Get the current button state.
107      *
108      */
getState()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      */
setState(int state)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      */
setState(final int state, final boolean callListener)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      */
setStateAnimatedInternal(final int state, final boolean callListener)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      */
setClickEnabled(boolean enabled)198     public void setClickEnabled(boolean enabled) {
199         mClickEnabled = enabled;
200     }
201 
setStateInternal(int state, boolean callListener)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 
nextState()226     private void nextState() {
227         int state = mState + 1;
228         if (state >= mImageIds.length) {
229             state = 0;
230         }
231         setState(state);
232     }
233 
init()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 
parseAttributes(Context context, AttributeSet attrs)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      */
overrideImageIds(int resId)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         if (mState >= 0 && mState < mImageIds.length) {
298             setImageByState(mState);
299         }
300     }
301 
302     /**
303      * Override the content descriptions of this button.
304      */
overrideContentDescriptions(int resId)305     public void overrideContentDescriptions(int resId) {
306         TypedArray ids = null;
307         try {
308             ids = getResources().obtainTypedArray(resId);
309             mDescIds = new int[ids.length()];
310             for (int i = 0; i < ids.length(); i++) {
311                 mDescIds[i] = ids.getResourceId(i, 0);
312             }
313         } finally {
314             if (ids != null) {
315                 ids.recycle();
316             }
317         }
318     }
319 
320     /**
321      * Set size info (either width or height, as necessary) of the view containing
322      * this button. Used for offset calculations during animation.
323      * @param s The size.
324      */
setParentSize(int s)325     public void setParentSize(int s) {
326         mParentSize = s;
327     }
328 
329     /**
330      * Set the animation direction.
331      * @param d Either ANIM_DIRECTION_VERTICAL or ANIM_DIRECTION_HORIZONTAL.
332      */
setAnimDirection(int d)333     public void setAnimDirection(int d) {
334         mAnimDirection = d;
335     }
336 
337     @Override
setImageLevel(int level)338     public void setImageLevel(int level) {
339         super.setImageLevel(level);
340         mLevel = level;
341     }
342 
setImageByState(int state)343     private void setImageByState(int state) {
344         if (mImageIds != null) {
345             setImageResource(mImageIds[state]);
346         }
347         super.setImageLevel(mLevel);
348     }
349 
combine(int oldState, int newState)350     private Bitmap combine(int oldState, int newState) {
351         // in some cases, a new set of image Ids are set via overrideImageIds()
352         // and oldState overruns the array.
353         // check here for that.
354         if (oldState >= mImageIds.length) {
355             return null;
356         }
357 
358         int width = getWidth();
359         int height = getHeight();
360 
361         if (width <= 0 || height <= 0) {
362             return null;
363         }
364 
365         int[] enabledState = new int[] {android.R.attr.state_enabled};
366 
367         // new state
368         Drawable newDrawable = getResources().getDrawable(mImageIds[newState]).mutate();
369         newDrawable.setState(enabledState);
370 
371         // old state
372         Drawable oldDrawable = getResources().getDrawable(mImageIds[oldState]).mutate();
373         oldDrawable.setState(enabledState);
374 
375         // combine 'em
376         Bitmap bitmap = null;
377         if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
378             int bitmapHeight = (height*2) + ((mParentSize - height)/2);
379             int oldBitmapOffset = height + ((mParentSize - height)/2);
380             bitmap = Bitmap.createBitmap(width, bitmapHeight, Bitmap.Config.ARGB_8888);
381             Canvas canvas = new Canvas(bitmap);
382             newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
383             oldDrawable.setBounds(0, oldBitmapOffset, oldDrawable.getIntrinsicWidth(), oldDrawable.getIntrinsicHeight()+oldBitmapOffset);
384             newDrawable.draw(canvas);
385             oldDrawable.draw(canvas);
386         } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
387             int bitmapWidth = (width*2) + ((mParentSize - width)/2);
388             int oldBitmapOffset = width + ((mParentSize - width)/2);
389             bitmap = Bitmap.createBitmap(bitmapWidth, height, Bitmap.Config.ARGB_8888);
390             Canvas canvas = new Canvas(bitmap);
391             newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
392             oldDrawable.setBounds(oldBitmapOffset, 0, oldDrawable.getIntrinsicWidth()+oldBitmapOffset, oldDrawable.getIntrinsicHeight());
393             newDrawable.draw(canvas);
394             oldDrawable.draw(canvas);
395         }
396 
397         return bitmap;
398     }
399 }