diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ToolbarNavigationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ToolbarNavigationController.java index d22a4446..875e367d 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ToolbarNavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ToolbarNavigationController.java @@ -61,7 +61,7 @@ public abstract class ToolbarNavigationController extends NavigationController i toolbar.processScrollCollapse(Toolbar.TOOLBAR_COLLAPSE_SHOW, true); toolbar.beginTransition(to.navigation); - toolbar.transitionProgress(0f, false); + toolbar.transitionProgress(0f); return true; } @@ -70,7 +70,7 @@ public abstract class ToolbarNavigationController extends NavigationController i public void swipeTransitionProgress(float progress) { super.swipeTransitionProgress(progress); - toolbar.transitionProgress(progress, false); + toolbar.transitionProgress(progress); } @Override diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/SearchLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/SearchLayout.java index b8b9c9e4..c5e63a5a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/SearchLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/SearchLayout.java @@ -130,12 +130,7 @@ public class SearchLayout extends LinearLayout { } public void openKeyboard() { - searchView.post(new Runnable() { - @Override - public void run() { - AndroidUtils.requestViewAndKeyboardFocus(searchView); - } - }); + searchView.post(() -> AndroidUtils.requestViewAndKeyboardFocus(searchView)); } public interface SearchLayoutCallback { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java index 3e95122c..51fe5098 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java @@ -43,7 +43,9 @@ public class NavigationItem { public ToolbarMiddleMenu middleMenu; public View rightView; - public ToolbarMenuItem createOverflow(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, List items) { + public ToolbarMenuItem createOverflow( + Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, + List items) { ToolbarMenuItem overflow = menu.createOverflow(callback); FloatingMenu overflowMenu = new FloatingMenu(context, overflow.getView(), items); overflow.setSubMenu(overflowMenu); @@ -53,4 +55,23 @@ public class NavigationItem { public void setTitle(int resId) { title = getString(resId); } + + public NavigationItem copy() { + NavigationItem c = new NavigationItem(); + c.title = title; + c.subtitle = subtitle; + + c.hasBack = hasBack; + c.hasDrawer = hasDrawer; + c.handlesToolbarInset = handlesToolbarInset; + c.swipeable = swipeable; + + c.search = search; + c.searchText = searchText; + +// c.menu = menu; + c.middleMenu = middleMenu; +// c.rightView = rightView; + return c; + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java index abd5581b..1252d0e6 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java @@ -17,10 +17,6 @@ */ package org.floens.chan.ui.toolbar; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; @@ -44,7 +40,6 @@ import org.floens.chan.R; import org.floens.chan.ui.drawable.ArrowMenuDrawable; import org.floens.chan.ui.drawable.DropdownArrowDrawable; import org.floens.chan.ui.layout.SearchLayout; -import org.floens.chan.ui.view.LoadView; import org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; @@ -52,10 +47,12 @@ import java.util.List; import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.getAttrColor; +import static org.floens.chan.utils.AndroidUtils.hideKeyboard; import static org.floens.chan.utils.AndroidUtils.removeFromParentView; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; -public class Toolbar extends LinearLayout implements View.OnClickListener { +public class Toolbar extends LinearLayout implements + View.OnClickListener, ToolbarPresenter.Callback { public static final int TOOLBAR_COLLAPSE_HIDE = 1000000; public static final int TOOLBAR_COLLAPSE_SHOW = -1000000; @@ -74,23 +71,18 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } }; + private ToolbarPresenter presenter; + private ImageView arrowMenuView; private ArrowMenuDrawable arrowMenuDrawable; - private LoadView navigationItemContainer; + private ToolbarContainer navigationItemContainer; private ToolbarCallback callback; - private boolean openKeyboardAfterSearchViewCreated = false; private int lastScrollDeltaOffset; private int scrollOffset; private List collapseCallbacks = new ArrayList<>(); - private boolean transitioning = false; - private NavigationItem fromItem; - private LinearLayout fromView; - private NavigationItem toItem; - private LinearLayout toView; - public Toolbar(Context context) { this(context, null); } @@ -106,7 +98,13 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { @Override public boolean dispatchTouchEvent(MotionEvent ev) { - return transitioning || super.dispatchTouchEvent(ev); + return isTransitioning() || super.dispatchTouchEvent(ev); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + return true; } public int getToolbarHeight() { @@ -143,7 +141,10 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { scrollOffset = Math.max(0, Math.min(getHeight(), scrollOffset)); if (animated) { - animate().translationY(-scrollOffset).setDuration(300).setInterpolator(new DecelerateInterpolator(2f)).start(); + animate().translationY(-scrollOffset) + .setDuration(300) + .setInterpolator(new DecelerateInterpolator(2f)) + .start(); boolean collapse = scrollOffset > 0; for (ToolbarCollapseCallback c : collapseCallbacks) { @@ -181,94 +182,45 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } } -// public void updateNavigation() { -// closeSearchInternal(); -// setNavigationItem(false, false, toItem); -// } - - public NavigationItem getNavigationItem() { - return toItem; + public void openSearch() { + presenter.openSearch(); } - public boolean openSearch() { - return openSearchInternal(); + public boolean closeSearch() { + return presenter.closeSearch(); } - public boolean closeSearch() { - return closeSearchInternal(); + public boolean isTransitioning() { + return navigationItemContainer.isTransitioning(); } public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) { - setNavigationItemInternal(animate, pushing, item); + ToolbarPresenter.AnimationStyle animationStyle; + if (!animate) { + animationStyle = ToolbarPresenter.AnimationStyle.NONE; + } else if (pushing) { + animationStyle = ToolbarPresenter.AnimationStyle.PUSH; + } else { + animationStyle = ToolbarPresenter.AnimationStyle.POP; + } + + presenter.set(item, animationStyle); } public void setArrowMenuIconShown(boolean show) { arrowMenuView.setVisibility(show ? View.VISIBLE : View.GONE); } - public boolean isTransitioning() { - return transitioning; - } - public void beginTransition(NavigationItem newItem) { - if (transitioning) { - throw new IllegalStateException("beginTransition called when already transitioning"); - } - - attachNavigationItem(newItem); - - navigationItemContainer.addView(toView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - - transitioning = true; + presenter.startTransition(newItem, ToolbarPresenter.TransitionAnimationStyle.POP); } - public void transitionProgress(float progress, boolean pushing) { - if (!transitioning) { - throw new IllegalStateException("transitionProgress called while not transitioning"); - } - - progress = Math.max(0f, Math.min(1f, progress)); - - final int offset = dp(16); - - toView.setTranslationY((pushing ? offset : -offset) * (1f - progress)); - toView.setAlpha(progress); - - if (fromItem != null) { - fromView.setTranslationY((pushing ? -offset : offset) * progress); - fromView.setAlpha(1f - progress); - } - - float arrowEnd = toItem.hasBack || toItem.search ? 1f : 0f; - if (arrowMenuDrawable.getProgress() != arrowEnd) { - arrowMenuDrawable.setProgress(toItem.hasBack || toItem.search ? progress : 1f - progress); - } + public void transitionProgress(float progress) { + presenter.setTransitionProgress(progress); } - public void finishTransition(boolean finished) { - if (!transitioning) { - throw new IllegalStateException("finishTransition called when not transitioning"); - } - - if (finished) { - if (fromItem != null) { - // From a search otherwise - if (fromItem != toItem) { - removeNavigationItem(fromItem, fromView); - fromView = null; - } - } - setArrowMenuProgress(toItem.hasBack || toItem.search ? 1f : 0f); - } else { - removeNavigationItem(toItem, toView); - setArrowMenuProgress(fromItem.hasBack || fromItem.search ? 1f : 0f); - toItem = fromItem; - toView = fromView; - } - - fromItem = null; - fromView = null; - transitioning = false; + public void finishTransition(boolean completed) { + presenter.stopTransition(completed); } public void setCallback(ToolbarCallback callback) { @@ -282,11 +234,6 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } } - @Override - public boolean onTouchEvent(MotionEvent event) { - return true; - } - public void setArrowMenuProgress(float progress) { arrowMenuDrawable.setProgress(progress); } @@ -300,20 +247,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } public void updateTitle(NavigationItem navigationItem) { - LinearLayout view = navigationItem == fromItem ? fromView : (navigationItem == toItem ? toView : null); - if (view != null) { - TextView titleView = view.findViewById(R.id.title); - if (titleView != null) { - titleView.setText(navigationItem.title); - } - - if (!TextUtils.isEmpty(navigationItem.subtitle)) { - TextView subtitleView = view.findViewById(R.id.subtitle); - if (subtitleView != null) { - subtitleView.setText(navigationItem.subtitle); - } - } - } + presenter.update(navigationItem); } private void init() { @@ -321,6 +255,13 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { if (isInEditMode()) return; + presenter = new ToolbarPresenter(); + presenter.create(this); + + initView(); + } + + private void initView() { FrameLayout leftButtonContainer = new FrameLayout(getContext()); addView(leftButtonContainer, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); @@ -333,11 +274,16 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { setRoundItemBackground(arrowMenuView); - leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(getResources().getDimensionPixelSize(R.dimen.toolbar_height), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL)); + int toolbarSize = getResources().getDimensionPixelSize(R.dimen.toolbar_height); + FrameLayout.LayoutParams leftButtonContainerLp = new FrameLayout.LayoutParams( + toolbarSize, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL); + leftButtonContainer.addView(arrowMenuView, leftButtonContainerLp); - navigationItemContainer = new LoadView(getContext()); + navigationItemContainer = new ToolbarContainer(getContext()); addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); + navigationItemContainer.setArrowMenu(arrowMenuDrawable); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (getElevation() == 0f) { setElevation(dp(4f)); @@ -345,116 +291,51 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } } - private boolean openSearchInternal() { - if (toItem != null && !toItem.search) { - toItem.search = true; - openKeyboardAfterSearchViewCreated = true; - setNavigationItemInternal(true, false, toItem); - callback.onSearchVisibilityChanged(toItem, true); - return true; - } else { - return false; - } - } + @Override + public void showForNavigationItem( + NavigationItem item, ToolbarPresenter.AnimationStyle animation) { + View view = createNavigationItemView(item); + boolean arrow = item.hasBack || item.search; - private boolean closeSearchInternal() { - if (toItem != null && toItem.search) { - toItem.search = false; - toItem.searchText = null; - setNavigationItemInternal(true, false, toItem); - callback.onSearchVisibilityChanged(toItem, false); - return true; - } else { - return false; - } + navigationItemContainer.set(view, arrow, animation); } - private void setNavigationItemInternal(boolean animate, final boolean pushing, NavigationItem newItem) { - if (transitioning) { - throw new IllegalStateException("setNavigationItemInternal called when already transitioning"); - } - - attachNavigationItem(newItem); - - transitioning = true; + @Override + public void containerStartTransition( + NavigationItem item, ToolbarPresenter.TransitionAnimationStyle animation) { + View view = createNavigationItemView(item); + boolean arrow = item.hasBack || item.search; - if (fromItem == toItem) { - // Search toggled - navigationItemContainer.setListener(new LoadView.Listener() { - @Override - public void onLoadViewRemoved(View view) { - // Remove the menu from the navigation item - ((ViewGroup) view).removeAllViews(); - finishTransition(true); - navigationItemContainer.setListener(null); - } - }); - navigationItemContainer.setView(toView, animate); + navigationItemContainer.startTransition(view, arrow, animation); + } - animateArrow(toItem.hasBack || toItem.search); - } else { - navigationItemContainer.addView(toView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - - if (animate) { - toView.setAlpha(0f); - - final ValueAnimator animator = ObjectAnimator.ofFloat(0f, 1f); - animator.setDuration(300); - animator.setInterpolator(new DecelerateInterpolator(2f)); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finishTransition(true); - } - }); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - transitionProgress((float) animation.getAnimatedValue(), pushing); - } - }); - if (!pushing) { - animator.setStartDelay(100); - } - - // hack to avoid the animation jumping when the current frame has a lot to do, - // which is often because setting a new navigationitem is almost always done - // when setting a new controller. - post(new Runnable() { - @Override - public void run() { - animator.start(); - } - }); - } else { - arrowMenuDrawable.setProgress(toItem.hasBack || toItem.search ? 1f : 0f); - finishTransition(true); - } - } + @Override + public void containerStopTransition(boolean didComplete) { + navigationItemContainer.stopTransition(didComplete); } - private void attachNavigationItem(NavigationItem newItem) { - if (transitioning) { - throw new IllegalStateException("attachNavigationItem called while transitioning"); - } + @Override + public void containerSetTransitionProgress(float progress) { + navigationItemContainer.setTransitionProgress(progress); + } - fromItem = toItem; - fromView = toView; - toItem = newItem; - toView = createNavigationItemView(toItem); + @Override + public void onSearchVisibilityChanged(NavigationItem item, boolean visible) { + callback.onSearchVisibilityChanged(item, visible); - if (!toItem.search) { - AndroidUtils.hideKeyboard(navigationItemContainer); + if (!visible) { + hideKeyboard(navigationItemContainer); } } - private void removeNavigationItem(NavigationItem item, LinearLayout view) { - if (!transitioning) { - throw new IllegalStateException("removeNavigationItem called while not transitioning"); - } + @Override + public void onSearchInput(NavigationItem item, String input) { + callback.onSearchEntered(item, input); + } - view.removeAllViews(); - navigationItemContainer.removeView(view); + @Override + public void updateViewForItem(NavigationItem item, boolean current) { + navigationItemContainer.update(item, current); } private LinearLayout createNavigationItemView(final NavigationItem item) { @@ -483,12 +364,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { int arrowPressedColor = getAttrColor(getContext(), R.attr.dropdown_light_pressed_color); Drawable drawable = new DropdownArrowDrawable(dp(12), dp(12), true, arrowColor, arrowPressedColor); titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); - titleView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - item.middleMenu.show(titleView); - } - }); + titleView.setOnClickListener(v -> item.middleMenu.show(titleView)); } TextView subtitleView = menu.findViewById(R.id.subtitle); @@ -521,12 +397,8 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { private LinearLayout createSearchLayout(NavigationItem item) { SearchLayout searchLayout = new SearchLayout(getContext()); - searchLayout.setCallback(new SearchLayout.SearchLayoutCallback() { - @Override - public void onSearchEntered(String entered) { - item.searchText = entered; - callback.onSearchEntered(item, entered); - } + searchLayout.setCallback(input -> { + presenter.searchInput(input); }); if (item.searchText != null) { @@ -534,29 +406,11 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } searchLayout.setHint(callback.getSearchHint(item)); - - if (openKeyboardAfterSearchViewCreated) { - openKeyboardAfterSearchViewCreated = false; - searchLayout.openKeyboard(); - } - searchLayout.setPadding(dp(16), searchLayout.getPaddingTop(), searchLayout.getPaddingRight(), searchLayout.getPaddingBottom()); return searchLayout; } - private void animateArrow(boolean toArrow) { - float to = toArrow ? 1f : 0f; - if (to != arrowMenuDrawable.getProgress()) { - ValueAnimator arrowAnimation = ValueAnimator.ofFloat(arrowMenuDrawable.getProgress(), to); - arrowAnimation.setDuration(300); - arrowAnimation.setInterpolator(new DecelerateInterpolator(2f)); - arrowAnimation.addUpdateListener(animation -> - setArrowMenuProgress((float) animation.getAnimatedValue())); - arrowAnimation.start(); - } - } - public interface ToolbarCallback { void onMenuOrBackClicked(boolean isArrow); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarContainer.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarContainer.java new file mode 100644 index 00000000..7be8e540 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarContainer.java @@ -0,0 +1,358 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.ui.toolbar; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.floens.chan.R; +import org.floens.chan.ui.drawable.ArrowMenuDrawable; + +import java.util.HashMap; +import java.util.Map; + +import static org.floens.chan.utils.AndroidUtils.dp; + +/** + * The container for the views created by the toolbar for the navigation items. + *

+ * It will strictly only transition between two views. If a new view is set + * and a transition is in progress, it is stopped before adding the new view. + *

+ * For normal animations the previousView is the view that is animated away from, and the + * currentView is the view where is animated to. The previousView is removed and cleared if the + * animation finished. + *

+ * Transitions are user-controlled animations that can be cancelled of finished. For that the + * currentView describes the view that was originally there, and the transitionView is the view + * what is possibly transitioned to. + *

+ * This is also the class that is responsible for the orientation and animation of the arrow-menu + * drawable. + */ +public class ToolbarContainer extends FrameLayout { + private ArrowMenuDrawable arrowMenu; + + private View previousView; + private boolean previousArrow; + private View currentView; + private boolean currentArrow; + private View transitionView; + private boolean transitionArrow; + private ToolbarPresenter.TransitionAnimationStyle transitionAnimationStyle; + + private Map animatorSet = new HashMap<>(); + + public ToolbarContainer(Context context) { + this(context, null); + } + + public ToolbarContainer(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ToolbarContainer(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setArrowMenu(ArrowMenuDrawable arrowMenu) { + this.arrowMenu = arrowMenu; + } + + public void set(View view, boolean arrow, ToolbarPresenter.AnimationStyle animation) { + if (transitionView != null) { + throw new IllegalStateException("Currently in transition mode"); + } + + endAnimations(); + + previousView = currentView; + previousArrow = currentArrow; + currentView = view; + currentArrow = arrow; + + addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + if (getChildCount() > 2) { + throw new IllegalArgumentException("More than 2 child views attached"); + } + + // Can't run the animation if there is no previous view + // Otherwise just show it without an animation. + if (animation != ToolbarPresenter.AnimationStyle.NONE && previousView != null) { + setAnimation(view, previousView, animation); + } else { + if (previousView != null) { + removeView(previousView); + previousView = null; + } + } + + if (animation == ToolbarPresenter.AnimationStyle.NONE) { + setArrowProgress(1f, !currentArrow); + } + } + + public void update(NavigationItem item, boolean current) { + // TODO: clean up + View view = current ? currentView : (previousView != null ? previousView : transitionView); + if (view != null) { + TextView titleView = view.findViewById(R.id.title); + if (titleView != null) { + titleView.setText(item.title); + } + + if (!TextUtils.isEmpty(item.subtitle)) { + TextView subtitleView = view.findViewById(R.id.subtitle); + if (subtitleView != null) { + subtitleView.setText(item.subtitle); + } + } + } + } + + public boolean isTransitioning() { + return transitionView != null || previousView != null; + } + + public void startTransition( + View view, boolean arrow, ToolbarPresenter.TransitionAnimationStyle style) { + if (transitionView != null) { + throw new IllegalStateException("Already in transition mode"); + } + + endAnimations(); + + transitionView = view; + transitionArrow = arrow; + transitionAnimationStyle = style; + addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + if (getChildCount() > 2) { + throw new IllegalArgumentException("More than 2 child views attached"); + } + } + + public void stopTransition(boolean didComplete) { + if (transitionView == null) { + throw new IllegalStateException("Not in transition mode"); + } + + if (didComplete) { + removeView(currentView); + currentView = transitionView; + currentArrow = transitionArrow; + transitionView = null; + } else { + removeView(transitionView); + transitionView = null; + } + + if (getChildCount() != 1) { + throw new IllegalStateException("Not 1 view attached"); + } + } + + public void setTransitionProgress(float progress) { + if (transitionView == null) { + throw new IllegalStateException("Not in transition mode"); + } + + transitionProgressAnimation(progress, transitionAnimationStyle); + } + + private void endAnimations() { + if (previousView != null) { + endAnimation(previousView); + if (previousView != null) { + throw new IllegalStateException("Animation end did not remove view"); + } + } + + if (currentView != null) { + endAnimation(currentView); + } + } + + private void endAnimation(View view) { + Animator a = animatorSet.remove(view); + if (a != null) { + a.end(); + } + } + + private void setAnimation(View view, View previousView, + ToolbarPresenter.AnimationStyle animationStyle) { + if (animationStyle == ToolbarPresenter.AnimationStyle.PUSH || + animationStyle == ToolbarPresenter.AnimationStyle.POP) { + final boolean pushing = animationStyle == ToolbarPresenter.AnimationStyle.PUSH; + + // Previous animation + ValueAnimator previousAnimation = getShortAnimator(); + previousAnimation.addUpdateListener(a -> { + float value = (float) a.getAnimatedValue(); + setPreviousAnimationProgress(previousView, pushing, value); + }); + previousAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animatorSet.remove(previousView); + removeView(previousView); + ToolbarContainer.this.previousView = null; + } + }); + if (!pushing) previousAnimation.setStartDelay(100); + animatorSet.put(previousView, previousAnimation); + + post(previousAnimation::start); + + // Current animation + arrow + view.setAlpha(0f); + ValueAnimator animation = getShortAnimator(); + animation.addUpdateListener(a -> { + float value = (float) a.getAnimatedValue(); + setAnimationProgress(view, pushing, value); + + if (previousArrow != currentArrow) { + setArrowProgress(value, !currentArrow); + } + }); + animation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animatorSet.remove(view); + } + }); + if (!pushing) animation.setStartDelay(100); + animatorSet.put(view, animation); + + post(animation::start); + } else if (animationStyle == ToolbarPresenter.AnimationStyle.FADE) { + // Previous animation + ValueAnimator previousAnimation = + ObjectAnimator.ofFloat(previousView, View.ALPHA, 1f, 0f); + previousAnimation.setDuration(300); + previousAnimation.setInterpolator(new LinearInterpolator()); + previousAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animatorSet.remove(previousView); + removeView(previousView); + ToolbarContainer.this.previousView = null; + } + }); + animatorSet.put(previousView, previousAnimation); + + post(previousAnimation::start); + + // Current animation + arrow + view.setAlpha(0f); + ValueAnimator animation = ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f); + animation.setDuration(300); + animation.setInterpolator(new LinearInterpolator()); + animation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animatorSet.remove(view); + } + }); + // A different animator for the arrow because that one needs the deceleration + // interpolator. + ValueAnimator arrow = ValueAnimator.ofFloat(0f, 1f); + arrow.setDuration(300); + arrow.setInterpolator(new DecelerateInterpolator(2f)); + arrow.addUpdateListener(a -> { + float value = (float) a.getAnimatedValue(); + if (previousArrow != currentArrow) { + setArrowProgress(value, !currentArrow); + } + }); + + AnimatorSet animationAndArrow = new AnimatorSet(); + animationAndArrow.playTogether(animation, arrow); + animationAndArrow.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animatorSet.remove(view); + } + }); + + animatorSet.put(view, animationAndArrow); + + post(animationAndArrow::start); + } + } + + private void setPreviousAnimationProgress(View view, boolean pushing, float progress) { + final int offset = dp(16); + view.setTranslationY((pushing ? -offset : offset) * progress); + view.setAlpha(1f - progress); + } + + private void setAnimationProgress(View view, boolean pushing, float progress) { + final int offset = dp(16); + view.setTranslationY((pushing ? offset : -offset) * (1f - progress)); + view.setAlpha(progress); + } + + private void setArrowProgress(float progress, boolean reverse) { + if (reverse) { + progress = 1f - progress; + } + progress = Math.max(0f, Math.min(1f, progress)); + + arrowMenu.setProgress(progress); + } + + private void transitionProgressAnimation( + float progress, ToolbarPresenter.TransitionAnimationStyle style) { + progress = Math.max(0f, Math.min(1f, progress)); + + final int offset = dp(16); + + boolean pushing = style == ToolbarPresenter.TransitionAnimationStyle.PUSH; + + transitionView.setTranslationY((pushing ? offset : -offset) * (1f - progress)); + transitionView.setAlpha(progress); + + currentView.setTranslationY((pushing ? -offset : offset) * progress); + currentView.setAlpha(1f - progress); + + if (transitionArrow != currentArrow) { + setArrowProgress(progress, !transitionArrow); + } + } + + private ValueAnimator getShortAnimator() { + final ValueAnimator animator = ObjectAnimator.ofFloat(0f, 1f); + animator.setDuration(300); + animator.setInterpolator(new DecelerateInterpolator(2f)); + return animator; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarPresenter.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarPresenter.java new file mode 100644 index 00000000..1c20496c --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarPresenter.java @@ -0,0 +1,159 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.ui.toolbar; + +public class ToolbarPresenter { + public enum AnimationStyle { + NONE, + PUSH, + POP, + FADE + } + + public enum TransitionAnimationStyle { + PUSH, + POP + } + + private Callback callback; + + private NavigationItem item; + private NavigationItem search; + private NavigationItem transition; + + void create(Callback callback) { + this.callback = callback; + } + + void set(NavigationItem newItem, AnimationStyle animation) { + cancelTransitionIfNeeded(); + if (closeSearchIfNeeded()) { + animation = AnimationStyle.FADE; + } + + item = newItem; + + callback.showForNavigationItem(item, animation); + } + + void update(NavigationItem updatedItem) { + callback.updateViewForItem(updatedItem, updatedItem == item); + } + + void startTransition(NavigationItem newItem, TransitionAnimationStyle animation) { + cancelTransitionIfNeeded(); + if (closeSearchIfNeeded()) { + callback.showForNavigationItem(item, AnimationStyle.NONE); + } + + transition = newItem; + + callback.containerStartTransition(transition, animation); + } + + void stopTransition(boolean didComplete) { + if (transition == null) { + return; + } + + callback.containerStopTransition(didComplete); + + if (didComplete) { + item = transition; + callback.showForNavigationItem(item, AnimationStyle.NONE); + } + transition = null; + } + + void setTransitionProgress(float progress) { + if (transition == null) { + return; + } + + callback.containerSetTransitionProgress(progress); + } + + void openSearch() { + if (search != null) { + return; + } + + cancelTransitionIfNeeded(); + + search = new NavigationItem(); + search.search = true; + callback.showForNavigationItem(search, AnimationStyle.FADE); + + callback.onSearchVisibilityChanged(item, true); + } + + boolean closeSearch() { + if (search == null) { + return false; + } + + search = null; + set(item, AnimationStyle.FADE); + + callback.onSearchVisibilityChanged(item, false); + + return true; + } + + private void cancelTransitionIfNeeded() { + if (transition != null) { + callback.containerStopTransition(false); + transition = null; + } + } + + private boolean closeSearchIfNeeded() { + // Cancel search + if (search != null) { + search = null; + callback.onSearchVisibilityChanged(item, false); + return true; + } + return false; + } + + void searchInput(String input) { + if (search == null) { + return; + } + + search.searchText = input; + callback.onSearchInput(item, search.searchText); + } + + interface Callback { + void showForNavigationItem(NavigationItem item, AnimationStyle animation); + + void containerStartTransition(NavigationItem item, TransitionAnimationStyle animation); + + void containerStopTransition(boolean didComplete); + + void containerSetTransitionProgress(float progress); + + void onSearchVisibilityChanged(NavigationItem item, boolean visible); + + void onSearchInput(NavigationItem item, String input); + + void updateViewForItem(NavigationItem item, boolean current); + } +}