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
17package com.android.camera;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.Matrix;
28import android.graphics.drawable.Drawable;
29import android.os.AsyncTask;
30import android.util.AttributeSet;
31import android.view.View;
32import android.widget.ImageButton;
33import android.widget.ImageView;
34
35import com.android.camera.util.Gusterpolator;
36import 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 */
48public 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}