/*
* Copyright 2014 LiaoKai
*
* 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.cocosw.undobar;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnticipateOvershootInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.cocosw.undobar.R.drawable;
import com.cocosw.undobar.R.id;
import com.cocosw.undobar.R.string;
import java.lang.reflect.Method;
public class UndoBarController extends LinearLayout {
private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height";
private static final String NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME = "navigation_bar_height_landscape";
private static final String SHOW_NAV_BAR_RES_NAME = "config_showNavigationBar";
public static final UndoBarStyle UNDOSTYLE = new UndoBarStyle(
drawable.ic_undobar_undo, string.undo);
private UndoBarStyle style = UndoBarController.UNDOSTYLE;
public static final UndoBarStyle RETRYSTYLE = new UndoBarStyle(drawable.ic_retry,
string.retry, -1);
public static final UndoBarStyle MESSAGESTYLE = new UndoBarStyle(-1, -1, 5000);
private static Animation inAnimation = inFromBottomAnimation(null);
private static Animation outAnimation = outToBottomAnimation(null);
private final TextView mMessageView;
private final TextView mButton;
private final Handler mHideHandler = new Handler();
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
hideUndoBar(false);
if (mUndoListener instanceof AdvancedUndoListener) {
((AdvancedUndoListener) mUndoListener).onHide(mUndoToken);
}
}
};
private UndoListener mUndoListener;
// State objects
private Parcelable mUndoToken;
private CharSequence mUndoMessage;
//Only for KitKat translucent mode
private int translucent = -1;
private boolean mInPortrait;
private String sNavBarOverride;
private boolean mNavBarAvailable;
private float mSmallestWidthDp;
private UndoBarController(final Context context, final AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.undobar, this, true);
mMessageView = (TextView) findViewById(R.id.undobar_message);
mButton = (TextView) findViewById(id.undobar_button);
mButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(final View view) {
if (mUndoListener != null) {
mUndoListener.onUndo(mUndoToken);
}
hideUndoBar(false);
}
}
);
hideUndoBar(true);
// https://github.com/jgilfelt/SystemBarTint/blob/master/library/src/com/readystatesoftware/systembartint/SystemBarTintManager.java
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mInPortrait = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
try {
Class c = Class.forName("android.os.SystemProperties");
Method m = c.getDeclaredMethod("get", String.class);
m.setAccessible(true);
sNavBarOverride = (String) m.invoke(null, "qemu.hw.mainkeys");
} catch (Throwable e) {
sNavBarOverride = null;
}
// check theme attrs
int[] as = {android.R.attr.windowTranslucentStatus,
android.R.attr.windowTranslucentNavigation};
TypedArray a = context.obtainStyledAttributes(as);
try {
mNavBarAvailable = a.getBoolean(1, false);
} finally {
a.recycle();
}
// check window flags
assert (getContext()) != null;
WindowManager.LayoutParams winParams = ((Activity) getContext()).getWindow().getAttributes();
int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
if ((winParams.flags & bits) != 0) {
mNavBarAvailable = true;
}
mSmallestWidthDp = getSmallestWidthDp(wm);
}
}
private static Animation outToBottomAnimation(
final android.view.animation.Animation.AnimationListener animationlistener) {
final TranslateAnimation translateanimation = new TranslateAnimation(2,
0F, 2, 0F, 2, 0F, 2, 1F);
translateanimation.setDuration(500L);
translateanimation.setInterpolator(new AnticipateOvershootInterpolator(
1.0f));
translateanimation.setAnimationListener(animationlistener);
return translateanimation;
}
private static Animation inFromBottomAnimation(
final android.view.animation.Animation.AnimationListener animationlistener) {
final TranslateAnimation translateanimation = new TranslateAnimation(2,
0F, 2, 0F, 2, 1F, 2, 0F);
translateanimation.setDuration(500L);
translateanimation.setInterpolator(new OvershootInterpolator(1.0f));
translateanimation.setAnimationListener(animationlistener);
return translateanimation;
}
/**
* Quick method to insert a UndoBar into an Activity
*
* @param activity Activity to hold this view
* @param message The message will be shown in left side in undobar
* @param listener Callback listener triggered after click undobar
* @param undoToken Token info,will pass to callback to help you to undo
* @param immediate Show undobar immediately or show it with animation
* @param style {@link UndoBarStyle}
* @return UndoBarController
*/
public static UndoBarController show(final Activity activity,
final CharSequence message, final UndoListener listener,
final Parcelable undoToken, final boolean immediate,
final UndoBarStyle style) {
return show(activity, message, listener, undoToken, immediate, style, -1);
}
public static UndoBarController show(final Activity activity,
final CharSequence message, UndoListener listener,
Parcelable undoToken, boolean immediate, UndoBarStyle style,
int translucent) {
UndoBarController undo = ensureView(activity);
if (style == null)
throw new IllegalArgumentException("style must not be empty.");
undo.style = style;
undo.setUndoListener(listener);
undo.showUndoBar(immediate, message, undoToken);
undo.translucent = translucent;
return undo;
}
private static UndoBarController ensureView(Activity activity) {
UndoBarController undo = UndoBarController.getView(activity);
if (undo == null) {
undo = new UndoBarController(activity, null);
((ViewGroup) activity.findViewById(android.R.id.content))
.addView(undo);
}
return undo;
}
private static UndoBarController getView(final Activity activity) {
final View view = activity.findViewById(id._undobar);
UndoBarController undo = null;
if (view != null) {
undo = (UndoBarController) view.getParent();
}
return undo;
}
public static UndoBarController show(final Activity activity,
final int message, final UndoListener listener,
final Parcelable undoToken, final boolean immediate) {
return UndoBarController.show(activity, activity.getText(message),
listener, undoToken, immediate, UndoBarController.UNDOSTYLE);
}
public static UndoBarController show(final Activity activity,
final CharSequence message, final UndoListener listener,
final Parcelable undoToken) {
return UndoBarController.show(activity, message, listener, undoToken,
false, UndoBarController.UNDOSTYLE);
}
public static UndoBarController show(final Activity activity,
final CharSequence message, final UndoListener listener,
final UndoBarStyle style) {
return UndoBarController.show(activity, message, listener, null, false,
style);
}
public static UndoBarController show(final Activity activity,
final CharSequence message, final UndoListener listener) {
return UndoBarController.show(activity, message, listener, null, false,
UndoBarController.UNDOSTYLE);
}
public static UndoBarController show(final Activity activity,
final CharSequence message) {
return UndoBarController.show(activity, message, null, null, false,
UndoBarController.MESSAGESTYLE);
}
/**
* Hide all undo bar immediately
*
* @param activity The activity where the undobar in
*/
public static void clear(final Activity activity) {
final UndoBarController v = UndoBarController.getView(activity);
if (v != null) {
v.setVisibility(View.GONE);
}
}
/**
* Change the default In/Out animation
*
* @param inAnimation
* @param outAnimation
*/
public static void setAnimation(Animation inAnimation, Animation outAnimation) {
if (inAnimation != null)
UndoBarController.inAnimation = inAnimation;
if (outAnimation != null)
UndoBarController.outAnimation = outAnimation;
}
@SuppressLint("NewApi")
private float getSmallestWidthDp(WindowManager wm) {
DisplayMetrics metrics = new DisplayMetrics();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
wm.getDefaultDisplay().getRealMetrics(metrics);
} else {
//this is not correct, but we don't really care pre-kitkat
wm.getDefaultDisplay().getMetrics(metrics);
}
float widthDp = metrics.widthPixels / metrics.density;
float heightDp = metrics.heightPixels / metrics.density;
return Math.min(widthDp, heightDp);
}
@TargetApi(14)
private int getNavigationBarHeight(Context context) {
Resources res = context.getResources();
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if (hasNavBar(context)) {
String key;
if (mInPortrait) {
key = NAV_BAR_HEIGHT_RES_NAME;
} else {
if (!isNavigationAtBottom())
return 0;
key = NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME;
}
return getInternalDimensionSize(res, key);
}
}
return result;
}
@TargetApi(14)
private boolean hasNavBar(Context context) {
Resources res = context.getResources();
int resourceId = res.getIdentifier(SHOW_NAV_BAR_RES_NAME, "bool", "android");
if (resourceId != 0) {
boolean hasNav = res.getBoolean(resourceId);
// check override flag (see static block)
if ("1".equals(sNavBarOverride)) {
hasNav = false;
} else if ("0".equals(sNavBarOverride)) {
hasNav = true;
}
return hasNav;
} else { // fallback
return !ViewConfiguration.get(context).hasPermanentMenuKey();
}
}
private int getInternalDimensionSize(Resources res, String key) {
int result = 0;
int resourceId = res.getIdentifier(key, "dimen", "android");
if (resourceId > 0) {
result = res.getDimensionPixelSize(resourceId);
}
return result;
}
/**
* Should a navigation bar appear at the bottom of the screen in the current
* device configuration? A navigation bar may appear on the right side of
* the screen in certain configurations.
*
* @return True if navigation should appear at the bottom of the screen, False otherwise.
*/
private boolean isNavigationAtBottom() {
return (mSmallestWidthDp >= 600 || mInPortrait);
}
/**
* Get callback listener
*
* @return
*/
public UndoListener getUndoListener() {
return mUndoListener;
}
private void setUndoListener(final UndoListener mUndoListener) {
this.mUndoListener = mUndoListener;
}
private void hideUndoBar(final boolean immediate) {
mHideHandler.removeCallbacks(mHideRunnable);
mUndoToken = null;
if (immediate) {
setVisibility(View.GONE);
} else {
clearAnimation();
if (style.outAnimation != null)
startAnimation(style.outAnimation);
else
startAnimation(outAnimation);
setVisibility(View.GONE);
}
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle outState = new Bundle();
outState.putCharSequence("undo_message", mUndoMessage);
outState.putParcelable("undo_token", mUndoToken);
outState.putParcelable("undo_style", style);
outState.putInt("visible", getVisibility());
return outState;
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
if (state instanceof Bundle) {
final Bundle bundle = (Bundle) state;
mUndoMessage = bundle.getCharSequence("undo_message");
mUndoToken = bundle.getParcelable("undo_token");
style = bundle.getParcelable("undo_style");
if (bundle.getInt("visible") == View.VISIBLE)
showUndoBar(true, mUndoMessage, mUndoToken);
return;
}
super.onRestoreInstanceState(state);
}
@SuppressWarnings("ConstantConditions")
private void showUndoBar(final boolean immediate,
final CharSequence message, final Parcelable undoToken) {
mUndoToken = undoToken;
mUndoMessage = message;
mMessageView.setText(mUndoMessage);
if (style.titleRes > 0) {
mButton.setVisibility(View.VISIBLE);
findViewById(id.undobar_divider).setVisibility(View.VISIBLE);
mButton.setText(style.titleRes);
if (style.iconRes > 0) {
mButton.setCompoundDrawablesWithIntrinsicBounds(getResources()
.getDrawable(style.iconRes), null, null, null);
}
} else {
mButton.setVisibility(View.GONE);
findViewById(id.undobar_divider).setVisibility(View.GONE);
}
if (style.bgRes > 0)
findViewById(id._undobar).setBackgroundResource(style.bgRes);
mHideHandler.removeCallbacks(mHideRunnable);
if (style.duration > 0) {
mHideHandler.postDelayed(mHideRunnable, style.duration);
}
if (!immediate) {
clearAnimation();
if (style.inAnimation != null)
startAnimation(style.inAnimation);
else
startAnimation(inAnimation);
}
setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && translucent != 0) {
if (translucent == 1 || mNavBarAvailable) {
setPadding(0, 0, 0, getNavigationBarHeight(getContext()));
}
}
}
public interface UndoListener {
/**
* The callback function will be called when user press button in Undobar
*
* @param token
*/
void onUndo(Parcelable token);
}
public interface AdvancedUndoListener extends UndoListener {
/**
* The callback function will be called when the Undobar fade out after duration without button clicked.
*
* @param token
*/
void onHide(Parcelable token);
}
/**
* UndoBar Builder
*/
public static class UndoBar {
private final Activity activity;
private UndoBarStyle style;
private CharSequence message;
private long duration;
private Parcelable undoToken;
private UndoListener listener;
private int translucent = -1;
public UndoBar(Activity activity) {
this.activity = activity;
}
public UndoBar style(UndoBarStyle style) {
this.style = style;
return this;
}
/**
* Set the message to be displayed on the left of the undobar.
*
* @param message message
* @return
*/
public UndoBar message(CharSequence message) {
this.message = message;
return this;
}
/**
* Set the message to be displayed on the left of the undobar.
*
* @param messageRes
* @return
*/
public UndoBar message(int messageRes) {
this.message = activity.getText(messageRes);
return this;
}
/**
* Sets the duration the undo bar will be shown.<br>
* Default is defined in style
*
* @param duraton
* @return
*/
public UndoBar duration(long duraton) {
this.duration = duraton;
return this;
}
/**
* Sets the listener which will be trigger when button been clicked.
*
* @param mUndoListener
* @return
*/
public UndoBar listener(UndoListener mUndoListener) {
this.listener = mUndoListener;
return this;
}
/**
* Sets a token for undobar which will be returned in listener
*
* @param undoToken
* @return
*/
public UndoBar token(Parcelable undoToken) {
this.undoToken = undoToken;
return this;
}
public UndoBar translucent(boolean enable) {
translucent = enable ? 1 : 0;
return this;
}
/**
* Show undobar with animation or not
*
* @param anim show animation or not
* @return
*/
public UndoBarController show(boolean anim) {
if (listener == null && style == null) {
style = MESSAGESTYLE;
}
if (style == null)
style = UNDOSTYLE;
if (message == null)
message = "";
if (duration > 0) {
style.duration = duration;
}
return UndoBarController.show(activity, message, listener, undoToken, !anim, style, translucent);
}
/**
* Show undobar with animation
*
* @return
*/
public UndoBarController show() {
return show(true);
}
}
}