package com.rey.material.app;
import android.os.Build;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.widget.ActionMenuView;
import android.support.v7.widget.Toolbar;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import com.rey.material.drawable.NavigationDrawerDrawable;
import com.rey.material.drawable.ToolbarRippleDrawable;
import com.rey.material.util.ViewUtil;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
* A Manager class to help handling Toolbar used as ActionBar in ActionBarActivity.
* It help grouping ActionItem in Toolbar and only show the items of current group.
* It also help manager state of navigation icon.
* Created by Rey on 1/6/2015.
*/
public class ToolbarManager {
private ActionBarActivity mActivity;
private Toolbar mToolbar;
private int mRippleStyle;
private Animator mAnimator;
private ActionMenuView mMenuView;
private ToolbarRippleDrawable.Builder mBuilder;
private int mCurrentGroup = 0;
private boolean mGroupChanged = false;
private boolean mMenuDataChanged = true;
/**
* Interface definition for a callback to be invoked when the current group is changed.
*/
public interface OnToolbarGroupChangedListener {
/**
* Called when the current group changed.
* @param oldGroupId The id of old group.
* @param groupId The id of new group.
*/
public void onToolbarGroupChanged(int oldGroupId, int groupId);
}
private ArrayList<WeakReference<OnToolbarGroupChangedListener>> mListeners = new ArrayList<>();
private ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ToolbarManager.this.onGlobalLayout();
}
};
private ArrayList<Animation> mAnimations = new ArrayList<>();
private Animation.AnimationListener mOutAnimationEndListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if(mActivity != null)
mActivity.supportInvalidateOptionsMenu();
else
onPrepareMenu();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
};
private NavigationManager mNavigationManager;
public ToolbarManager(ActionBarActivity activity, Toolbar toolbar, int defaultGroupId, int rippleStyle, int animIn, int animOut){
this(activity, toolbar, defaultGroupId, rippleStyle, new SimpleAnimator(animIn, animOut));
}
public ToolbarManager(ActionBarActivity activity, Toolbar toolbar, int defaultGroupId, int rippleStyle, Animator animator){
mActivity = activity;
mToolbar = toolbar;
mCurrentGroup = defaultGroupId;
mRippleStyle = rippleStyle;
mAnimator = animator;
mActivity.setSupportActionBar(toolbar);
}
/**
* Register a listener for current group changed event. Note that it doesn't hold any strong reference to listener, so don't use anonymous listener.
*/
public void registerOnToolbarGroupChangedListener(OnToolbarGroupChangedListener listener){
for(int i = mListeners.size() - 1; i >= 0; i--){
WeakReference<OnToolbarGroupChangedListener> ref = mListeners.get(i);
if(ref.get() == null)
mListeners.remove(i);
else if(ref.get() == listener)
return;
}
mListeners.add(new WeakReference<OnToolbarGroupChangedListener>(listener));
}
/**
* Unregister a listener.
* @param listener
*/
public void unregisterOnToolbarGroupChangedListener(OnToolbarGroupChangedListener listener){
for(int i = mListeners.size() - 1; i >= 0; i--){
WeakReference<OnToolbarGroupChangedListener> ref = mListeners.get(i);
if(ref.get() == null || ref.get() == listener)
mListeners.remove(i);
}
}
private void dispatchOnToolbarGroupChanged(int oldGroupId, int groupId){
for(int i = mListeners.size() - 1; i >= 0; i--){
WeakReference<OnToolbarGroupChangedListener> ref = mListeners.get(i);
if(ref.get() == null)
mListeners.remove(i);
else
ref.get().onToolbarGroupChanged(oldGroupId, groupId);
}
}
/**
* @return The current group of the Toolbar.
*/
public int getCurrentGroup(){
return mCurrentGroup;
}
/**
* Set current group of the Toolbar.
* @param groupId The id of group.
*/
public void setCurrentGroup(int groupId){
if(mCurrentGroup != groupId){
int oldGroupId = mCurrentGroup;
mCurrentGroup = groupId;
mGroupChanged = true;
dispatchOnToolbarGroupChanged(oldGroupId, mCurrentGroup);
animateOut();
}
}
/**
* This funcction should be called in onCreateOptionsMenu of Activity or Fragment to inflate a new menu.
* @param menuId
*/
public void createMenu(int menuId){
mToolbar.inflateMenu(menuId);
mMenuDataChanged = true;
if(mActivity == null)
onPrepareMenu();
}
/**
* This function should be called in onPrepareOptionsMenu(Menu) of Activity that use
* Toolbar as ActionBar, or after inflating menu.
*/
public void onPrepareMenu(){
if(mGroupChanged || mMenuDataChanged){
mToolbar.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
Menu menu = mToolbar.getMenu();
for(int i = 0, count = menu.size(); i < count; i++){
MenuItem item = menu.getItem(i);
item.setVisible(item.getGroupId() == mCurrentGroup);
}
mMenuDataChanged = false;
}
}
/**
* Set a NavigationManager to manage navigation icon state.
*/
public void setNavigationManager(NavigationManager navigationManager){
mNavigationManager = navigationManager;
}
/**
* Notify the current state of navigation icon is invalid. It should update the state immediately without showing animation.
*/
public void notifyNavigationStateInvalidated(){
if(mNavigationManager != null)
mNavigationManager.notifyStateInvalidated();
}
/**
* Notify the current state of navigation icon is invalid. It should update the state immediately without showing animation.
*/
public void notifyNavigationStateChanged(){
if(mNavigationManager != null)
mNavigationManager.notifyStateChanged();
}
/**
* Notify the progress of animation between 2 states changed. Use this function to sync the progress with another animation.
* @param isBackState the current state (the end state of animation) is back state or not.
* @param progress the current progress of animation.
*/
public void notifyNavigationStateProgressChanged(boolean isBackState, float progress){
if(mNavigationManager != null)
mNavigationManager.notifyStateProgressChanged(isBackState, progress);
}
/**
* @return The navigation is in back state or not.
*/
public boolean isNavigationBackState(){
return mNavigationManager != null && mNavigationManager.isBackState();
}
private ToolbarRippleDrawable getBackground(){
if(mBuilder == null)
mBuilder = new ToolbarRippleDrawable.Builder(mToolbar.getContext(), mRippleStyle);
return mBuilder.build();
}
private ActionMenuView getMenuView(){
if(mMenuView == null){
for (int i = 0; i < mToolbar.getChildCount(); i++) {
View child = mToolbar.getChildAt(i);
if (child instanceof ActionMenuView) {
mMenuView = (ActionMenuView) child;
break;
}
}
}
return mMenuView;
}
private void onGlobalLayout() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
mToolbar.getViewTreeObserver().removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
else
mToolbar.getViewTreeObserver().removeGlobalOnLayoutListener(mOnGlobalLayoutListener);
ActionMenuView menuView = getMenuView();
for(int i = 0, count = menuView == null ? 0 : menuView.getChildCount(); i < count; i++){
View child = menuView.getChildAt(i);
if(mRippleStyle != 0){
if(child.getBackground() == null || !(child.getBackground() instanceof ToolbarRippleDrawable))
ViewUtil.setBackground(child, getBackground());
}
}
if(mGroupChanged){
animateIn();
mGroupChanged = false;
}
}
private void animateOut(){
ActionMenuView menuView = getMenuView();
int count = menuView == null ? 0 : menuView.getChildCount();
Animation slowestAnimation = null;
mAnimations.clear();
mAnimations.ensureCapacity(count);
for(int i = 0; i < count; i++){
View child = menuView.getChildAt(i);
Animation anim = mAnimator.getOutAnimation(child, i);
mAnimations.add(anim);
if(anim != null)
if(slowestAnimation == null || slowestAnimation.getStartOffset() + slowestAnimation.getDuration() < anim.getStartOffset() + anim.getDuration())
slowestAnimation = anim;
}
if(slowestAnimation == null)
mOutAnimationEndListener.onAnimationEnd(null);
else {
slowestAnimation.setAnimationListener(mOutAnimationEndListener);
for(int i = 0; i < count; i++){
Animation anim = mAnimations.get(i);
if(anim != null)
menuView.getChildAt(i).startAnimation(anim);
}
}
mAnimations.clear();
}
private void animateIn(){
ActionMenuView menuView = getMenuView();
for(int i = 0, count = menuView == null ? 0 : menuView.getChildCount(); i < count; i++){
View child = menuView.getChildAt(i);
Animation anim = mAnimator.getInAnimation(child, i);
if(anim != null)
child.startAnimation(anim);
}
}
/**
* Interface definition for creating animation of menu item view when switch group.
*/
public interface Animator{
/**
* Get the animation of the menu item view will be removed.
* @param v The menu item view.
* @param position The position of item.
* @return
*/
public Animation getOutAnimation(View v, int position);
/**
* Get the animation of the menu item view will be added.
* @param v The menu item view.
* @param position The position of item.
* @return
*/
public Animation getInAnimation(View v, int position);
}
private static class SimpleAnimator implements Animator{
private int mAnimationIn;
private int mAnimationOut;
public SimpleAnimator(int animIn, int animOut){
mAnimationIn = animIn;
mAnimationOut = animOut;
}
@Override
public Animation getOutAnimation(View v, int position) {
return mAnimationOut == 0 ? null : AnimationUtils.loadAnimation(v.getContext(), mAnimationOut);
}
@Override
public Animation getInAnimation(View v, int position) {
return mAnimationIn == 0 ? null : AnimationUtils.loadAnimation(v.getContext(), mAnimationIn);
}
}
/**
* Abstract class to manage the state of navigation icon.
*/
public static abstract class NavigationManager{
protected NavigationDrawerDrawable mNavigationIcon;
protected Toolbar mToolbar;
/**
* @param styleId the style res of navigation icon.
*/
public NavigationManager(int styleId, Toolbar toolbar){
mToolbar = toolbar;
mNavigationIcon = new NavigationDrawerDrawable.Builder(mToolbar.getContext(), styleId).build();
mToolbar.setNavigationIcon(mNavigationIcon);
mToolbar.setNavigationOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
onNavigationClick();
}
});
}
/**
* Check if current state of navigation icon is back state or not.
* @return
*/
public abstract boolean isBackState();
/**
* Hangle event click navigation icon. Subclass should override this function.
*/
public abstract void onNavigationClick();
/**
* Notify the current state of navigation icon is invalid. It should update the state immediately without showing animation.
*/
public void notifyStateInvalidated(){
mNavigationIcon.switchIconState(isBackState() ? NavigationDrawerDrawable.STATE_ARROW : NavigationDrawerDrawable.STATE_DRAWER, false);
}
/**
* Notify the current state of navigation icon is changed. It should update the state with animation.
*/
public void notifyStateChanged(){
mNavigationIcon.switchIconState(isBackState() ? NavigationDrawerDrawable.STATE_ARROW : NavigationDrawerDrawable.STATE_DRAWER, true);
}
/**
* Notify the progress of animation between 2 states changed. Use this function to sync the progress with another animation.
* @param isBackState the current state (the end state of animation) is back state or not.
* @param progress the current progress of animation.
*/
public void notifyStateProgressChanged(boolean isBackState, float progress){
mNavigationIcon.setIconState(isBackState ? NavigationDrawerDrawable.STATE_ARROW : NavigationDrawerDrawable.STATE_DRAWER, progress);
}
}
/**
* A Base Navigation Manager that handle navigation state between fragment changing and navigation drawer.
* If you want to handle state in another case, you should override isBackState(), shouldSyncDrawerSlidingProgress(), and call notify notifyStateChanged() if need.
*/
public static class BaseNavigationManager extends NavigationManager{
protected DrawerLayout mDrawerLayout;
protected FragmentManager mFragmentManager;
protected boolean mSyncDrawerSlidingProgress = false;
/**
*
* @param styledId the style res of navigation icon.
* @param drawerLayout can be null if you don't need to handle navigation state when open/close navigation drawer.
*/
public BaseNavigationManager(int styledId, ActionBarActivity activity, Toolbar toolbar, DrawerLayout drawerLayout){
super(styledId, toolbar);
mDrawerLayout = drawerLayout;
mFragmentManager = activity.getSupportFragmentManager();
if(mDrawerLayout != null)
mDrawerLayout.setDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
BaseNavigationManager.this.onDrawerSlide(drawerView, slideOffset);
}
@Override
public void onDrawerOpened(View drawerView) {
BaseNavigationManager.this.onDrawerOpened(drawerView);
}
@Override
public void onDrawerClosed(View drawerView) {
BaseNavigationManager.this.onDrawerClosed(drawerView);
}
@Override
public void onDrawerStateChanged(int newState) {
BaseNavigationManager.this.onDrawerStateChanged(newState);
}
});
mFragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
onFragmentChanged();
}
});
}
@Override
public boolean isBackState() {
return mFragmentManager.getBackStackEntryCount() > 1 || (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(Gravity.START));
}
@Override
public void onNavigationClick() {}
/**
* Check if should sync progress of drawer sliding animation with navigation state changing animation.
*/
protected boolean shouldSyncDrawerSlidingProgress(){
if(mFragmentManager.getBackStackEntryCount() > 1)
return false;
return true;
}
protected void onFragmentChanged(){
notifyStateChanged();
}
/**
* Handling onDrawerSlide event of DrawerLayout. It'll sync progress of drawer sliding animation with navigation state changing animation if needed.
* If you also want to handle this event, make sure to call super method.
*/
protected void onDrawerSlide(View drawerView, float slideOffset){
if(!mSyncDrawerSlidingProgress)
return;
if(mDrawerLayout.isDrawerOpen(GravityCompat.START))
notifyStateProgressChanged(false, 1f - slideOffset);
else
notifyStateProgressChanged(true, slideOffset);
}
protected void onDrawerOpened(View drawerView){}
protected void onDrawerClosed(View drawerView) {}
/**
* Handling onDrawerStateChanged event of DrawerLayout. It'll check if should sync progress of drawer sliding animation with navigation state changing animation.
* If you also want to handle this event, make sure to call super method.
*/
protected void onDrawerStateChanged(int newState) {
mSyncDrawerSlidingProgress = (newState == DrawerLayout.STATE_DRAGGING || newState == DrawerLayout.STATE_SETTLING) && shouldSyncDrawerSlidingProgress();
}
}
}