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 }