/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.camera; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.util.AttributeSet; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import com.android.camera.util.Gusterpolator; import com.android.camera2.R; /* * A toggle button that supports two or more states with images rendererd on top * for each state. * The button is initialized in an XML layout file with an array reference of * image ids (e.g. imageIds="@array/camera_flashmode_icons"). * Each image in the referenced array represents a single integer state. * Every time the user touches the button it gets set to next state in line, * with the corresponding image drawn onto the face of the button. * State wraps back to 0 on user touch when button is already at n-1 state. */ public class MultiToggleImageButton extends ImageButton { /* * Listener interface for button state changes. */ public interface OnStateChangeListener { /* * @param view the MultiToggleImageButton that received the touch event * @param state the new state the button is in */ public abstract void stateChanged(View view, int state); } public static final int ANIM_DIRECTION_VERTICAL = 0; public static final int ANIM_DIRECTION_HORIZONTAL = 1; private static final int ANIM_DURATION_MS = 250; private static final int UNSET = -1; private OnStateChangeListener mOnStateChangeListener; private OnStateChangeListener mOnStatePreChangeListener; private int mState = UNSET; private int[] mImageIds; private int[] mDescIds; private int mLevel; private boolean mClickEnabled = true; private int mParentSize; private int mAnimDirection; private Matrix mMatrix = new Matrix(); private ValueAnimator mAnimator; public MultiToggleImageButton(Context context) { super(context); init(); } public MultiToggleImageButton(Context context, AttributeSet attrs) { super(context, attrs); init(); parseAttributes(context, attrs); setState(0); } public MultiToggleImageButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); parseAttributes(context, attrs); setState(0); } /* * Set the state change listener. * * @param onStateChangeListener The listener to set. */ public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) { mOnStateChangeListener = onStateChangeListener; } /** * Set the listener that will be invoked right after the click event before * all the operations required to change the state of the button. This * listener is useful if the client doesn't want to wait until the state * change is completed to perform certain tasks. * * @param onStatePreChangeListener The listener to set. */ public void setOnPreChangeListener(OnStateChangeListener onStatePreChangeListener) { mOnStatePreChangeListener = onStatePreChangeListener; } /* * Get the current button state. * */ public int getState() { return mState; } /* * Set the current button state, thus causing the state change listener to * get called. * * @param state the desired state */ public void setState(int state) { setState(state, true); } /* * Set the current button state. * * @param state the desired state * @param callListener should the state change listener be called? */ public void setState(final int state, final boolean callListener) { setStateAnimatedInternal(state, callListener); } /** * Set the current button state via an animated transition. * * @param state * @param callListener */ private void setStateAnimatedInternal(final int state, final boolean callListener) { if(callListener && mOnStatePreChangeListener != null) { mOnStatePreChangeListener.stateChanged(MultiToggleImageButton.this, mState); } if (mState == state || mState == UNSET) { setStateInternal(state, callListener); return; } if (mImageIds == null) { return; } new AsyncTask() { @Override protected Bitmap doInBackground(Integer... params) { return combine(params[0], params[1]); } @Override protected void onPostExecute(Bitmap bitmap) { if (bitmap == null) { setStateInternal(state, callListener); } else { setImageBitmap(bitmap); int offset; if (mAnimDirection == ANIM_DIRECTION_VERTICAL) { offset = (mParentSize+getHeight())/2; } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) { offset = (mParentSize+getWidth())/2; } else { return; } mAnimator.setFloatValues(-offset, 0.0f); AnimatorSet s = new AnimatorSet(); s.play(mAnimator); s.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { setClickEnabled(false); } @Override public void onAnimationEnd(Animator animation) { setStateInternal(state, callListener); setClickEnabled(true); } }); s.start(); } } }.execute(mState, state); } /** * Enable or disable click reactions for this button * without affecting visual state. * For most cases you'll want to use {@link #setEnabled(boolean)}. * @param enabled True if click enabled, false otherwise. */ public void setClickEnabled(boolean enabled) { mClickEnabled = enabled; } private void setStateInternal(int state, boolean callListener) { mState = state; if (mImageIds != null) { setImageByState(mState); } if (mDescIds != null) { String oldContentDescription = String.valueOf(getContentDescription()); String newContentDescription = getResources().getString(mDescIds[mState]); if (oldContentDescription != null && !oldContentDescription.isEmpty() && !oldContentDescription.equals(newContentDescription)) { setContentDescription(newContentDescription); String announceChange = getResources().getString( R.string.button_change_announcement, newContentDescription); announceForAccessibility(announceChange); } } super.setImageLevel(mLevel); if (callListener && mOnStateChangeListener != null) { mOnStateChangeListener.stateChanged(MultiToggleImageButton.this, getState()); } } private void nextState() { int state = mState + 1; if (state >= mImageIds.length) { state = 0; } setState(state); } protected void init() { this.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mClickEnabled) { nextState(); } } }); setScaleType(ImageView.ScaleType.MATRIX); mAnimator = ValueAnimator.ofFloat(0.0f, 0.0f); mAnimator.setDuration(ANIM_DURATION_MS); mAnimator.setInterpolator(Gusterpolator.INSTANCE); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMatrix.reset(); if (mAnimDirection == ANIM_DIRECTION_VERTICAL) { mMatrix.setTranslate(0.0f, (Float) animation.getAnimatedValue()); } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) { mMatrix.setTranslate((Float) animation.getAnimatedValue(), 0.0f); } setImageMatrix(mMatrix); invalidate(); } }); } private void parseAttributes(Context context, AttributeSet attrs) { TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.MultiToggleImageButton, 0, 0); int imageIds = a.getResourceId(R.styleable.MultiToggleImageButton_imageIds, 0); if (imageIds > 0) { overrideImageIds(imageIds); } int descIds = a.getResourceId(R.styleable.MultiToggleImageButton_contentDescriptionIds, 0); if (descIds > 0) { overrideContentDescriptions(descIds); } a.recycle(); } /** * Override the image ids of this button. */ public void overrideImageIds(int resId) { TypedArray ids = null; try { ids = getResources().obtainTypedArray(resId); mImageIds = new int[ids.length()]; for (int i = 0; i < ids.length(); i++) { mImageIds[i] = ids.getResourceId(i, 0); } } finally { if (ids != null) { ids.recycle(); } } if (mState >= 0 && mState < mImageIds.length) { setImageByState(mState); } } /** * Override the content descriptions of this button. */ public void overrideContentDescriptions(int resId) { TypedArray ids = null; try { ids = getResources().obtainTypedArray(resId); mDescIds = new int[ids.length()]; for (int i = 0; i < ids.length(); i++) { mDescIds[i] = ids.getResourceId(i, 0); } } finally { if (ids != null) { ids.recycle(); } } } /** * Set size info (either width or height, as necessary) of the view containing * this button. Used for offset calculations during animation. * @param s The size. */ public void setParentSize(int s) { mParentSize = s; } /** * Set the animation direction. * @param d Either ANIM_DIRECTION_VERTICAL or ANIM_DIRECTION_HORIZONTAL. */ public void setAnimDirection(int d) { mAnimDirection = d; } @Override public void setImageLevel(int level) { super.setImageLevel(level); mLevel = level; } private void setImageByState(int state) { if (mImageIds != null) { setImageResource(mImageIds[state]); } super.setImageLevel(mLevel); } private Bitmap combine(int oldState, int newState) { // In some cases, a new set of image Ids are set via overrideImageIds() // and oldState or newState overrun the array. // check here for that. if (oldState >= mImageIds.length || newState >= mImageIds.length) { return null; } int width = getWidth(); int height = getHeight(); if (width <= 0 || height <= 0) { return null; } int[] enabledState = new int[] {android.R.attr.state_enabled}; // new state Drawable newDrawable = getResources().getDrawable(mImageIds[newState]).mutate(); newDrawable.setState(enabledState); // old state Drawable oldDrawable = getResources().getDrawable(mImageIds[oldState]).mutate(); oldDrawable.setState(enabledState); // combine 'em Bitmap bitmap = null; if (mAnimDirection == ANIM_DIRECTION_VERTICAL) { int bitmapHeight = (height*2) + ((mParentSize - height)/2); int oldBitmapOffset = height + ((mParentSize - height)/2); bitmap = Bitmap.createBitmap(width, bitmapHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight()); oldDrawable.setBounds(0, oldBitmapOffset, oldDrawable.getIntrinsicWidth(), oldDrawable.getIntrinsicHeight()+oldBitmapOffset); newDrawable.draw(canvas); oldDrawable.draw(canvas); } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) { int bitmapWidth = (width*2) + ((mParentSize - width)/2); int oldBitmapOffset = width + ((mParentSize - width)/2); bitmap = Bitmap.createBitmap(bitmapWidth, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight()); oldDrawable.setBounds(oldBitmapOffset, 0, oldDrawable.getIntrinsicWidth()+oldBitmapOffset, oldDrawable.getIntrinsicHeight()); newDrawable.draw(canvas); oldDrawable.draw(canvas); } return bitmap; } }