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 }