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