/*
|
* 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.os.SystemProperties;
|
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;
|
|
private static final String ANDROID_UVC_PROPERTY = "ro.camera.uvcfacing";
|
|
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<Integer, Void, Bitmap>() {
|
@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(getId() == R.id.camera_toggle_button){
|
/*
|
*If ro.camera.uvcfacing is set, then skip the original facing state.
|
*/
|
String uvcProperty = SystemProperties.get(ANDROID_UVC_PROPERTY, "");
|
if(uvcProperty.equals("")){
|
if(state == Integer.parseInt(getResources()
|
.getString(R.string.pref_camera_id_entry_external_value))){
|
state = state + 1;
|
}
|
}
|
else if (uvcProperty.equals("back")) {
|
if(state == Integer.parseInt(getResources()
|
.getString(R.string.pref_camera_id_entry_back_value))){
|
state = state + 1;
|
}
|
}
|
else if (uvcProperty.equals("front")) {
|
if(state == Integer.parseInt(getResources()
|
.getString(R.string.pref_camera_id_entry_front_value))){
|
state = state + 1;
|
}
|
}
|
else if (uvcProperty.equals("external")) {
|
int externalValue = Integer.parseInt(getResources().
|
getString(R.string.pref_camera_id_entry_external_value));
|
if(state != externalValue){
|
state = externalValue;
|
}
|
}
|
}
|
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;
|
}
|
}
|