package at.markushi.ui;
import com.linju.android_property2.R;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import at.markushi.ui.action.Action;
import at.markushi.ui.action.BackAction;
import at.markushi.ui.action.CloseAction;
import at.markushi.ui.action.DrawerAction;
import at.markushi.ui.action.LineSegment;
import at.markushi.ui.action.PlusAction;
import at.markushi.ui.util.BakedBezierInterpolator;
import at.markushi.ui.util.UiHelper;
/**
* ActionView allows you to dynamically display action and automatically animate from one action to another.
* <p/>
* ActionView can be included in your layout xml quite easy:
* <pre>
* {@code
* <at.markushi.ui.ActionView
* android:id="@+id/action"
* android:layout_width="56dip"
* android:layout_height="56dip"
* android:padding="16dip"
* app:av_color="@android:color/white"
* app:av_action="drawer"/>
* }</pre>
*
* In order to start a transition to another {@link at.markushi.ui.action.Action} call {@link #setAction(at.markushi.ui.action.Action)}
*
* @see at.markushi.ui.action.BackAction
* @see at.markushi.ui.action.CloseAction
* @see at.markushi.ui.action.DrawerAction
* @see at.markushi.ui.action.PlusAction
*/
@SuppressLint("NewApi")
public class ActionView extends View {
public static final int ROTATE_CLOCKWISE = 0;
public static final int ROTATE_COUNTER_CLOCKWISE = 1;
private static final int STROKE_SIZE_DIP = 2;
private int color;
private Action oldAction;
private Action currentAction;
private Action animatedAction;
private float animationProgress;
private float scale;
private int padding;
private Path path;
private Paint paint;
private float centerX;
private float centerY;
private boolean ready = false;
private boolean animateWhenReady = false;
private int rotation = ROTATE_CLOCKWISE;
private long animationDuration;
private int size;
public ActionView(Context context) {
this(context, null);
}
public ActionView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ActionView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
animationDuration = getResources().getInteger(R.integer.av_animationDuration);
animatedAction = new Action(new float[Action.ACTION_SIZE], null);
final float strokeSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, STROKE_SIZE_DIP, context.getResources().getDisplayMetrics());
path = new Path();
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(0xDDFFFFFF);
paint.setStrokeWidth(strokeSize);
paint.setStyle(Paint.Style.STROKE);
if (attrs == null) {
return;
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionView);
final int color = a.getColor(R.styleable.ActionView_av_color, 0xDDFFFFF);
final int actionId = a.getInt(R.styleable.ActionView_av_action, 0);
a.recycle();
paint.setColor(color);
final Action action = getActionFromEnum(actionId);
setAction(action);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.centerX = w / 2;
this.centerY = h / 2;
padding = getPaddingLeft();
size = Math.min(w, h);
scale = Math.min(w, h) - (2 * padding);
ready = true;
transformActions();
if (animateWhenReady) {
animateWhenReady = false;
startAnimation();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (currentAction == null) {
return;
}
if (oldAction == null) {
updatePath(currentAction);
} else {
final float inverseProgress = 1f - animationProgress;
final float[] current = currentAction.getLineData();
final float[] old = oldAction.getLineData();
final float[] animated = animatedAction.getLineData();
for (int i = 0; i < animated.length; i++) {
animated[i] = current[i] * animationProgress + old[i] * inverseProgress;
}
updatePath(animatedAction);
}
canvas.rotate((rotation == ROTATE_CLOCKWISE ? 180f : -180f) * animationProgress, centerX, centerY);
canvas.drawPath(path, paint);
}
@SuppressWarnings("UnusedDeclaration")
public float getAnimationProgress() {
return animationProgress;
}
@SuppressWarnings("UnusedDeclaration")
public void setAnimationProgress(float animationProgress) {
this.animationProgress = animationProgress;
UiHelper.postInvalidateOnAnimation(this);
}
/**
* Set the color used for drawing an {@link at.markushi.ui.action.Action}.
*
* @param color
*/
public void setColor(final int color) {
this.color = color;
paint.setColor(color);
UiHelper.postInvalidateOnAnimation(this);
}
/**
* @return the current action
*/
public Action getAction() {
return currentAction;
}
/**
* Sets the new action. If an action was set before a transition will be started.
*
* @param action
* @see #setAction(at.markushi.ui.action.Action, boolean)
* @see #setAction(at.markushi.ui.action.Action, boolean, int)
* @see #setAction(at.markushi.ui.action.Action, at.markushi.ui.action.Action, int, long)
*/
public void setAction(final Action action) {
setAction(action, true, ROTATE_CLOCKWISE);
}
/**
* Sets the new action. If an action was set before a transition will be started.
*
* @param action
* @param rotation Can be either {@link #ROTATE_CLOCKWISE} or {@link #ROTATE_COUNTER_CLOCKWISE}.
*/
public void setAction(final Action action, final int rotation) {
setAction(action, true, rotation);
}
/**
* Sets the new action.
*
* @param action
* @param animate If a prior action was set and {@code true} a transition will be started, otherwise not.
*/
public void setAction(final Action action, final boolean animate) {
setAction(action, animate, ROTATE_CLOCKWISE);
}
/**
* Sets a new action transition.
*
* @param fromAction The initial action.
* @param toAction The target action.
* @param rotation The rotation direction used for the transition {@link #ROTATE_CLOCKWISE} or {@link #ROTATE_COUNTER_CLOCKWISE}.
* @param delay The delay in ms before the transition is started.
*/
public void setAction(final Action fromAction, final Action toAction, final int rotation, long delay) {
setAction(fromAction, false, ROTATE_CLOCKWISE);
postDelayed(new Runnable() {
@Override
public void run() {
if (!isAttachedToWindow()) {
return;
}
setAction(toAction, true, rotation);
}
}, delay);
}
/**
* Set the animation duration used for Action transitions
*
* @param animationDuration the duration in ms
*/
public void setAnimationDuration(long animationDuration) {
this.animationDuration = animationDuration;
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState ss = new SavedState(superState);
ss.currentAction = currentAction;
ss.color = color;
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
this.color = ss.color;
this.currentAction = ss.currentAction;
this.animationProgress = 1f;
}
private void setAction(Action action, boolean animate, int rotation) {
if (action == null) {
return;
}
this.rotation = rotation;
if (currentAction == null) {
currentAction = action;
currentAction.flipHorizontally();
animationProgress = 1f;
UiHelper.postInvalidateOnAnimation(this);
return;
}
if (currentAction.getClass().equals(action.getClass())) {
return;
}
oldAction = currentAction;
currentAction = action;
if (animate) {
animationProgress = 0f;
if (ready) {
startAnimation();
} else {
animateWhenReady = true;
}
} else {
animationProgress = 1f;
UiHelper.postInvalidateOnAnimation(this);
}
}
private void updatePath(Action action) {
path.reset();
final float[] data = action.getLineData();
// Once we're near the end of the animation we use the action segments to draw linked lines
if (animationProgress > 0.95f && !action.getLineSegments().isEmpty()) {
for (LineSegment s : action.getLineSegments()) {
path.moveTo(data[s.getStartIdx() + 0], data[s.getStartIdx() + 1]);
path.lineTo(data[s.getStartIdx() + 2], data[s.getStartIdx() + 3]);
for (int i = 1; i < s.indexes.length; i++) {
path.lineTo(data[s.indexes[i] + 0], data[s.indexes[i] + 1]);
path.lineTo(data[s.indexes[i] + 2], data[s.indexes[i] + 3]);
}
}
} else {
for (int i = 0; i < data.length; i += 4) {
path.moveTo(data[i + 0], data[i + 1]);
path.lineTo(data[i + 2], data[i + 3]);
}
}
}
private void transformActions() {
if (currentAction != null && !currentAction.isTransformed()) {
currentAction.transform(padding, padding, scale, size);
}
if (oldAction != null && !oldAction.isTransformed()) {
oldAction.transform(padding, padding, scale, size);
}
}
private void startAnimation() {
oldAction.flipHorizontally();
currentAction.flipHorizontally();
transformActions();
animatedAction.setLineSegments(currentAction.getLineSegments());
final ObjectAnimator animator = ObjectAnimator.ofFloat(ActionView.this, "animationProgress", 0f, 1f);
animator.setInterpolator(BakedBezierInterpolator.getInstance());
animator.setDuration(animationDuration).start();
}
private Action getActionFromEnum(int id) {
switch (id) {
case 0:
return new DrawerAction();
case 1:
return new BackAction();
case 2:
return new CloseAction();
case 3:
return new PlusAction();
}
return null;
}
static class SavedState extends BaseSavedState {
Action currentAction;
int color;
public SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel source) {
super(source);
currentAction = source.readParcelable(getClass().getClassLoader());
color = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(currentAction, 0);
dest.writeInt(color);
}
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel parcel) {
return new SavedState(parcel);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}