blob: 3816819e70cd99a2f8332b28941bc7a7dfaa3c25 [file] [log] [blame]
Marco Nelissen0744e4a2013-11-22 01:47:37 +00001/*
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
Spike Sprague18b22b52014-08-08 18:13:56 -070019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
Spike Sprague3c3b31d2014-09-08 10:43:04 -070021import android.animation.AnimatorSet;
Spike Sprague18b22b52014-08-08 18:13:56 -070022import android.animation.ValueAnimator;
Marco Nelissen0744e4a2013-11-22 01:47:37 +000023import android.content.Context;
24import android.content.res.TypedArray;
Spike Sprague18b22b52014-08-08 18:13:56 -070025import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.Matrix;
Spike Sprague66a3e6c2014-01-15 10:11:54 -080028import android.graphics.drawable.Drawable;
Spike Sprague3c3b31d2014-09-08 10:43:04 -070029import android.os.AsyncTask;
Marco Nelissen0744e4a2013-11-22 01:47:37 +000030import android.util.AttributeSet;
Marco Nelissen0744e4a2013-11-22 01:47:37 +000031import android.view.View;
Spike Sprague18b22b52014-08-08 18:13:56 -070032import android.widget.ImageButton;
33import android.widget.ImageView;
Marco Nelissen0744e4a2013-11-22 01:47:37 +000034
Spike Sprague18b22b52014-08-08 18:13:56 -070035import com.android.camera.util.Gusterpolator;
Marco Nelissen0744e4a2013-11-22 01:47:37 +000036import 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 /*
Spike Sprague18b22b52014-08-08 18:13:56 -070050 * Listener interface for button state changes.
Marco Nelissen0744e4a2013-11-22 01:47:37 +000051 */
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
Spike Sprague18b22b52014-08-08 18:13:56 -070060 public static final int ANIM_DIRECTION_VERTICAL = 0;
61 public static final int ANIM_DIRECTION_HORIZONTAL = 1;
62
Spike Sprague3c3b31d2014-09-08 10:43:04 -070063 private static final int ANIM_DURATION_MS = 250;
Spike Sprague18b22b52014-08-08 18:13:56 -070064 private static final int UNSET = -1;
65
Marco Nelissen0744e4a2013-11-22 01:47:37 +000066 private OnStateChangeListener mOnStateChangeListener;
Spike Sprague18b22b52014-08-08 18:13:56 -070067 private int mState = UNSET;
Marco Nelissen0744e4a2013-11-22 01:47:37 +000068 private int[] mImageIds;
Spike Sprague79718f62014-01-28 18:48:22 -080069 private int[] mDescIds;
Spike Sprague66a3e6c2014-01-15 10:11:54 -080070 private int mLevel;
Spike Sprague18b22b52014-08-08 18:13:56 -070071 private boolean mClickEnabled = true;
72 private int mParentSize;
73 private int mAnimDirection;
Spike Sprague3c3b31d2014-09-08 10:43:04 -070074 private Matrix mMatrix = new Matrix();
75 private ValueAnimator mAnimator;
Marco Nelissen0744e4a2013-11-22 01:47:37 +000076
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 */
Spike Sprague18b22b52014-08-08 18:13:56 -0700129 public void setState(final int state, final boolean callListener) {
Spike Sprague3c3b31d2014-09-08 10:43:04 -0700130 setStateAnimatedInternal(state, callListener);
Alan Newbergerc91c8d22014-09-09 18:47:37 -0700131 }
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) {
Spike Sprague18b22b52014-08-08 18:13:56 -0700140 if (mState == state || mState == UNSET) {
141 setStateInternal(state, callListener);
142 return;
143 }
144
145 if (mImageIds == null) {
146 return;
147 }
148
Spike Sprague3c3b31d2014-09-08 10:43:04 -0700149 new AsyncTask<Integer, Void, Bitmap>() {
Spike Sprague18b22b52014-08-08 18:13:56 -0700150 @Override
Spike Sprague3c3b31d2014-09-08 10:43:04 -0700151 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();
Spike Sprague18b22b52014-08-08 18:13:56 -0700187 }
Spike Sprague18b22b52014-08-08 18:13:56 -0700188 }
Spike Sprague3c3b31d2014-09-08 10:43:04 -0700189 }.execute(mState, state);
Spike Sprague18b22b52014-08-08 18:13:56 -0700190 }
191
Spike Sprague66d3d0d2014-08-18 14:11:33 -0700192 /**
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
Spike Sprague18b22b52014-08-08 18:13:56 -0700202 private void setStateInternal(int state, boolean callListener) {
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000203 mState = state;
Erin Dahlgrenf80ac9e2014-02-18 11:20:34 -0800204 if (mImageIds != null) {
Spike Sprague18b22b52014-08-08 18:13:56 -0700205 setImageByState(mState);
Erin Dahlgrenf80ac9e2014-02-18 11:20:34 -0800206 }
Spike Sprague18b22b52014-08-08 18:13:56 -0700207
Erin Dahlgrenf80ac9e2014-02-18 11:20:34 -0800208 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(
Alan Newberger5c62dc72014-02-04 09:15:47 -0800215 R.string.button_change_announcement, newContentDescription);
Erin Dahlgrenf80ac9e2014-02-18 11:20:34 -0800216 announceForAccessibility(announceChange);
217 }
Alan Newberger5c62dc72014-02-04 09:15:47 -0800218 }
Spike Sprague66a3e6c2014-01-15 10:11:54 -0800219 super.setImageLevel(mLevel);
Spike Sprague18b22b52014-08-08 18:13:56 -0700220
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000221 if (callListener && mOnStateChangeListener != null) {
Spike Sprague18b22b52014-08-08 18:13:56 -0700222 mOnStateChangeListener.stateChanged(MultiToggleImageButton.this, getState());
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000223 }
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) {
Spike Sprague18b22b52014-08-08 18:13:56 -0700238 if (mClickEnabled) {
239 nextState();
240 }
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000241 }
242 });
Spike Sprague18b22b52014-08-08 18:13:56 -0700243 setScaleType(ImageView.ScaleType.MATRIX);
Spike Sprague3c3b31d2014-09-08 10:43:04 -0700244
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 });
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000262 }
263
264 private void parseAttributes(Context context, AttributeSet attrs) {
265 TypedArray a = context.getTheme().obtainStyledAttributes(
266 attrs,
267 R.styleable.MultiToggleImageButton,
268 0, 0);
Spike Sprague79718f62014-01-28 18:48:22 -0800269 int imageIds = a.getResourceId(R.styleable.MultiToggleImageButton_imageIds, 0);
Erin Dahlgrenf80ac9e2014-02-18 11:20:34 -0800270 if (imageIds > 0) {
271 overrideImageIds(imageIds);
272 }
Spike Sprague79718f62014-01-28 18:48:22 -0800273 int descIds = a.getResourceId(R.styleable.MultiToggleImageButton_contentDescriptionIds, 0);
Erin Dahlgrenf80ac9e2014-02-18 11:20:34 -0800274 if (descIds > 0) {
275 overrideContentDescriptions(descIds);
276 }
Erin Dahlgrena07e94c2013-12-04 18:44:08 -0800277 a.recycle();
278 }
279
280 /**
281 * Override the image ids of this button.
282 */
283 public void overrideImageIds(int resId) {
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000284 TypedArray ids = null;
285 try {
Erin Dahlgrena07e94c2013-12-04 18:44:08 -0800286 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 }
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000291 } finally {
292 if (ids != null) {
293 ids.recycle();
294 }
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000295 }
Spike Spraguef3d360e2014-10-23 16:35:09 -0700296
297 if (mState >= 0 && mState < mImageIds.length) {
298 setImageByState(mState);
299 }
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000300 }
Spike Sprague66a3e6c2014-01-15 10:11:54 -0800301
Spike Sprague79718f62014-01-28 18:48:22 -0800302 /**
303 * Override the content descriptions of this button.
304 */
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
Spike Sprague18b22b52014-08-08 18:13:56 -0700320 /**
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 */
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 */
333 public void setAnimDirection(int d) {
334 mAnimDirection = d;
335 }
336
Spike Sprague66a3e6c2014-01-15 10:11:54 -0800337 @Override
338 public void setImageLevel(int level) {
339 super.setImageLevel(level);
340 mLevel = level;
341 }
Spike Sprague18b22b52014-08-08 18:13:56 -0700342
343 private void setImageByState(int state) {
344 if (mImageIds != null) {
345 setImageResource(mImageIds[state]);
346 }
347 super.setImageLevel(mLevel);
348 }
349
350 private Bitmap combine(int oldState, int newState) {
Spike Sprague39384da2014-08-13 16:57:20 -0700351 // 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
Spike Sprague18b22b52014-08-08 18:13:56 -0700358 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 }
Marco Nelissen0744e4a2013-11-22 01:47:37 +0000399}