diff --git a/Clover/app/src/main/java/org/floens/chan/controller/Controller.java b/Clover/app/src/main/java/org/floens/chan/controller/Controller.java index 8a6f9e03..31c986c8 100644 --- a/Clover/app/src/main/java/org/floens/chan/controller/Controller.java +++ b/Clover/app/src/main/java/org/floens/chan/controller/Controller.java @@ -36,7 +36,7 @@ import java.util.ArrayList; import java.util.List; public abstract class Controller { - private static final boolean LOG_STATES = false; + private static final boolean LOG_STATES = true; public Context context; public ViewGroup view; diff --git a/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java index d207743f..a7a0ad0d 100644 --- a/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java @@ -89,6 +89,8 @@ public abstract class NavigationController extends Controller { throw new IllegalArgumentException("Cannot transition while another transition is in progress."); } + blockingInput = true; + to.onShow(); return true; @@ -110,7 +112,7 @@ public abstract class NavigationController extends Controller { } public void transition(final Controller from, final Controller to, final boolean pushing, ControllerTransition controllerTransition) { - if (this.controllerTransition != null) { + if (this.controllerTransition != null || blockingInput) { throw new IllegalArgumentException("Cannot transition while another transition is in progress."); } diff --git a/Clover/app/src/main/java/org/floens/chan/controller/ui/NavigationControllerContainerLayout.java b/Clover/app/src/main/java/org/floens/chan/controller/ui/NavigationControllerContainerLayout.java index 2825fbcf..9c71a332 100644 --- a/Clover/app/src/main/java/org/floens/chan/controller/ui/NavigationControllerContainerLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/controller/ui/NavigationControllerContainerLayout.java @@ -18,6 +18,7 @@ package org.floens.chan.controller.ui; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -32,30 +33,57 @@ import android.widget.Scroller; import org.floens.chan.controller.Controller; import org.floens.chan.controller.NavigationController; +import org.floens.chan.utils.Logger; import org.floens.chan.utils.Time; +import static org.floens.chan.utils.AndroidUtils.dp; + public class NavigationControllerContainerLayout extends FrameLayout { + // The shadow starts at this alpha and goes up to 1f + public static final float SHADOW_MIN_ALPHA = 0.5f; + + private NavigationController navigationController; private int slopPixels; + private int minimalMovedPixels; private int flingPixels; private int maxFlingPixels; private boolean swipeEnabled = true; - private MotionEvent downEvent; - private boolean dontStartSwiping = false; - private MotionEvent swipeStartEvent; + // The event used in onInterceptTouchEvent to track the initial down event + private MotionEvent interceptedEvent; + + // The tracking is blocked when the user has moved too much in the y direction + private boolean blockTracking = false; + + // Is the top controller being tracked and moved + private boolean tracking = false; + + // The controller being tracked, corresponds with tracking + private Controller trackingController; + + // The controller behind the tracking controller + private Controller behindTrackingController; + + // The position of the touch after tracking has started, used to calculate the total offset from + private int trackStartPosition; + + // Tracks the motion when tracking private VelocityTracker velocityTracker; + + // Used to fling and scroll the tracking view private Scroller scroller; - private boolean popAfterSwipe = false; + + // Indicate if the controller should be popped after the animation ends + private boolean finishTransitionAfterAnimation = false; + + // Paint, draw rect and position for drawing the shadow + // The shadow is only drawn when tracking is true private Paint shadowPaint; private Rect shadowRect = new Rect(); - private boolean drawShadow; - private int swipePosition; - private boolean swiping = false; - - private Controller swipingController; + private int shadowPosition; public NavigationControllerContainerLayout(Context context) { super(context); @@ -74,7 +102,8 @@ public class NavigationControllerContainerLayout extends FrameLayout { private void init() { ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); - slopPixels = (int) (viewConfiguration.getScaledTouchSlop() * 0.5f); + slopPixels = viewConfiguration.getScaledTouchSlop(); + minimalMovedPixels = dp(3); flingPixels = viewConfiguration.getScaledMinimumFlingVelocity(); maxFlingPixels = viewConfiguration.getScaledMaximumFlingVelocity(); @@ -93,51 +122,34 @@ public class NavigationControllerContainerLayout extends FrameLayout { @Override public boolean onInterceptTouchEvent(MotionEvent event) { - if (!swipeEnabled || swiping) { + if (!swipeEnabled || tracking || navigationController.isBlockingInput() || !navigationController.getTop().navigationItem.swipeable || getBelowTop() == null) { return false; } int actionMasked = event.getActionMasked(); - if (actionMasked != MotionEvent.ACTION_DOWN && downEvent == null) { + if (actionMasked != MotionEvent.ACTION_DOWN && interceptedEvent == null) { // Action down wasn't called here, ignore return false; } - if (!navigationController.getTop().navigationItem.swipeable) { - return false; - } - - if (getBelowTop() == null) { - // Cannot swipe now - return false; - } - switch (actionMasked) { case MotionEvent.ACTION_DOWN: // Logger.test("onInterceptTouchEvent down"); - downEvent = MotionEvent.obtain(event); + interceptedEvent = MotionEvent.obtain(event); break; case MotionEvent.ACTION_MOVE: { // Logger.test("onInterceptTouchEvent move"); - float x = (event.getX() - downEvent.getX()); - float y = (event.getY() - downEvent.getY()); + float x = (event.getX() - interceptedEvent.getX()); + float y = (event.getY() - interceptedEvent.getY()); if (Math.abs(y) >= slopPixels) { -// Logger.test("dontStartSwiping = true"); - dontStartSwiping = true; +// Logger.test("blockTracking = true"); + blockTracking = true; } - if (!dontStartSwiping && Math.abs(x) > Math.abs(y) && x >= slopPixels && !navigationController.isBlockingInput()) { -// Logger.test("Start tracking swipe"); - downEvent.recycle(); - downEvent = null; - - swipeStartEvent = MotionEvent.obtain(event); - velocityTracker = VelocityTracker.obtain(); - velocityTracker.addMovement(event); - - swiping = true; + if (!blockTracking && x >= minimalMovedPixels && Math.abs(x) > Math.abs(y)) { + startTracking(event); return true; } @@ -146,9 +158,9 @@ public class NavigationControllerContainerLayout extends FrameLayout { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { // Logger.test("onInterceptTouchEvent cancel/up"); - downEvent.recycle(); - downEvent = null; - dontStartSwiping = false; + interceptedEvent.recycle(); + interceptedEvent = null; + blockTracking = false; break; } } @@ -159,41 +171,34 @@ public class NavigationControllerContainerLayout extends FrameLayout { @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { - if (downEvent != null) { - downEvent.recycle(); - downEvent = null; + if (interceptedEvent != null) { + interceptedEvent.recycle(); + interceptedEvent = null; + } + blockTracking = false; + if (tracking) { + endTracking(false); } - dontStartSwiping = false; } super.requestDisallowInterceptTouchEvent(disallowIntercept); } @Override - public boolean onTouchEvent(MotionEvent event) { - // This touch wasn't initiated with onInterceptTouchEvent - if (swipeStartEvent == null) { - return false; - } - - if (swipingController == null) { - // Start of swipe - -// Logger.test("Start of swipe"); + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); - swipingController = navigationController.getTop(); - drawShadow = true; - - long start = Time.startTiming(); - - Controller below = getBelowTop(); - navigationController.beginSwipeTransition(swipingController, below); +// endTracking(); + } - Time.endTiming("attach", start); + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!tracking) { + return false; } - float translationX = Math.max(0, event.getX() - swipeStartEvent.getX()); - setTopControllerTranslation((int) translationX); + int translationX = Math.max(0, ((int) event.getX()) - trackStartPosition); + setTopControllerTranslation(translationX); velocityTracker.addMovement(event); @@ -206,37 +211,42 @@ public class NavigationControllerContainerLayout extends FrameLayout { velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000); - float velocity = velocityTracker.getXVelocity(); + int velocity = (int) velocityTracker.getXVelocity(); + + if (translationX > 0) { + boolean doFlingAway = false; - if (translationX > 0f) { - boolean isFling = false; + Logger.test("velocity = %d", velocity); - if (velocity > 0f && Math.abs(velocity) > flingPixels && Math.abs(velocity) < maxFlingPixels) { - scroller.fling((int) translationX, 0, (int) velocity, 0, 0, Integer.MAX_VALUE, 0, 0); + if ((velocity > 0 && Math.abs(velocity) > 2500 && Math.abs(velocity) < maxFlingPixels) || translationX >= getWidth() * 3 / 4) { +// int left = getWidth() - translationX; +// int flingVelocity = Math.max(velocity, 0); - if (scroller.getFinalX() >= getWidth()) { - isFling = true; + scroller.fling(translationX, 0, velocity, 0, 0, Integer.MAX_VALUE, 0, 0); + Logger.test("finalX = %d getWidth = %d", scroller.getFinalX(), getWidth()); + + // Make sure the animation always goes past the end + if (scroller.getFinalX() < getWidth()) { + scroller.startScroll(translationX, 0, getWidth(), 0, 2000); } + + doFlingAway = true; + Logger.test("Flinging away with velocity = %d", velocity); } - if (isFling) { -// Logger.test("Flinging with velocity = " + velocity); - popAfterSwipe = true; - ViewCompat.postOnAnimation(this, flingRunnable); + if (doFlingAway) { + startFlingAnimation(true); } else { -// Logger.test("Snapping back!"); + Logger.test("Snapping back"); scroller.forceFinished(true); - scroller.startScroll((int) translationX, 0, -((int) translationX), 0, 300); - popAfterSwipe = false; - ViewCompat.postOnAnimation(this, flingRunnable); + scroller.startScroll(translationX, 0, -translationX, 0, 250); + startFlingAnimation(false); } } else { - finishSwipe(); + // User swiped back to the left + endTracking(false); } - swipeStartEvent.recycle(); - swipeStartEvent = null; - velocityTracker.recycle(); velocityTracker = null; @@ -251,26 +261,64 @@ public class NavigationControllerContainerLayout extends FrameLayout { protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); - if (drawShadow) { - float alpha = Math.min(1f, Math.max(0f, 0.5f - (swipePosition / (float) getWidth()) * 0.5f)); + if (tracking) { + float alpha = Math.min(1f, Math.max(0f, SHADOW_MIN_ALPHA - (shadowPosition / (float) getWidth()) * SHADOW_MIN_ALPHA)); shadowPaint.setColor(Color.argb((int) (alpha * 255f), 0, 0, 0)); - shadowRect.set(0, 0, swipePosition, getHeight()); + shadowRect.set(0, 0, shadowPosition, getHeight()); canvas.drawRect(shadowRect, shadowPaint); } } - private void finishSwipe() { - Controller below = getBelowTop(); + private void startTracking(MotionEvent startEvent) { + if (tracking) { + throw new IllegalStateException("startTracking called but already tracking"); + } + + tracking = true; + trackingController = navigationController.getTop(); + behindTrackingController = getBelowTop(); + + interceptedEvent.recycle(); + interceptedEvent = null; + + trackStartPosition = (int) startEvent.getX(); + velocityTracker = VelocityTracker.obtain(); + velocityTracker.addMovement(startEvent); + + long start = Time.startTiming(); + + navigationController.beginSwipeTransition(trackingController, behindTrackingController); + + Time.endTiming("attach", start); + + Logger.test("Start tracking " + trackingController.getClass().getSimpleName()); + } + + private void endTracking(boolean finishTransition) { + Logger.test("endTracking finishTransition = " + finishTransition); - navigationController.endSwipeTransition(swipingController, below, popAfterSwipe); - swipingController = null; - drawShadow = false; - swiping = false; + if (!tracking) { + throw new IllegalStateException("endTracking called but was not tracking"); + } + + navigationController.endSwipeTransition(trackingController, behindTrackingController, finishTransition); + tracking = false; + trackingController = null; + behindTrackingController = null; + } + + private void startFlingAnimation(boolean finishTransitionAfterAnimation) { + this.finishTransitionAfterAnimation = finishTransitionAfterAnimation; + ViewCompat.postOnAnimation(this, flingRunnable); } private Runnable flingRunnable = new Runnable() { @Override public void run() { + if (!tracking) { + throw new IllegalStateException("fling animation running while not tracking"); + } + boolean finished = false; if (scroller.computeScrollOffset()) { @@ -289,15 +337,15 @@ public class NavigationControllerContainerLayout extends FrameLayout { if (!finished) { ViewCompat.postOnAnimation(NavigationControllerContainerLayout.this, flingRunnable); } else { - finishSwipe(); + endTracking(finishTransitionAfterAnimation); } } }; private void setTopControllerTranslation(int translationX) { - swipePosition = translationX; - swipingController.view.setTranslationX(swipePosition); - navigationController.swipeTransitionProgress(swipePosition / (float) getWidth()); + shadowPosition = translationX; + trackingController.view.setTranslationX(translationX); + navigationController.swipeTransitionProgress(translationX / (float) getWidth()); invalidate(); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index 3dae204b..080a7539 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -21,7 +21,6 @@ import android.content.SharedPreferences; import android.os.Environment; import org.floens.chan.Chan; -import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.cell.PostCellInterface; @@ -118,7 +117,7 @@ public class ChanSettings { theme = new StringSetting(p, "preference_theme", "light"); - boolean tablet = AndroidUtils.getRes().getBoolean(R.bool.is_tablet); + boolean tablet = false; fontSize = new StringSetting(p, "preference_font", tablet ? "16" : "14"); openLinkConfirmation = new BooleanSetting(p, "preference_open_link_confirmation", true); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java index 141eac5e..10635a43 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java @@ -302,23 +302,28 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat .setPositiveButton(R.string.exit, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - stackTop().onHide(); StartActivity.super.onBackPressed(); } }) .show(); } else { - // Don't destroy the view, let Android do that or it'll create artifacts - stackTop().onHide(); super.onBackPressed(); } } } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + runtimePermissionsHelper.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + @Override protected void onDestroy() { super.onDestroy(); + stackTop().onHide(); stackTop().onDestroy(); stack.clear(); } @@ -344,13 +349,6 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat imagePickDelegate.onActivityResult(requestCode, resultCode, data); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - runtimePermissionsHelper.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - private Controller stackTop() { return stack.get(stack.size() - 1); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/StyledToolbarNavigationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/StyledToolbarNavigationController.java index 223a4d52..efe8e70b 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/StyledToolbarNavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/StyledToolbarNavigationController.java @@ -55,6 +55,18 @@ public class StyledToolbarNavigationController extends ToolbarNavigationControll } } + @Override + public void endSwipeTransition(Controller from, Controller to, boolean finish) { + super.endSwipeTransition(from, to, finish); + + if (finish) { + DrawerController drawerController = getDrawerController(); + if (drawerController != null) { + drawerController.setDrawerEnabled(to.navigationItem.hasDrawer); + } + } + } + @Override public boolean onBack() { if (super.onBack()) { 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 b11e0817..6e5fc8f0 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 @@ -75,12 +75,7 @@ public abstract class ToolbarNavigationController extends NavigationController i super.endSwipeTransition(from, to, finish); toolbar.finishTransition(finish); - - if (finish) { - updateToolbarCollapse(to, controllerTransition != null); - } else { - updateToolbarCollapse(from, controllerTransition != null); - } + updateToolbarCollapse(finish ? to : from, controllerTransition != null); } @Override 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 47223db3..dbb17dcc 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 @@ -49,7 +49,7 @@ import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; -public class Toolbar extends LinearLayout implements View.OnClickListener, LoadView.Listener { +public class Toolbar extends LinearLayout implements View.OnClickListener { public static final int TOOLBAR_COLLAPSE_HIDE = 1000000; public static final int TOOLBAR_COLLAPSE_SHOW = -1000000; @@ -83,6 +83,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV private int lastScrollDeltaOffset; private int scrollOffset; + private boolean transitioning = false; private NavigationItem fromItem; private NavigationItem toItem; @@ -156,12 +157,22 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV } public void beginTransition(NavigationItem newItem) { + if (transitioning) { + throw new IllegalStateException("beginTransition called when already transitioning"); + } + attachNavigationItem(newItem); navigationItemContainer.addView(toItem.view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + transitioning = true; } public void transitionProgress(float progress, boolean pushing) { + if (!transitioning) { + throw new IllegalStateException("transitionProgress called while not transitioning"); + } + final int offset = dp(16); toItem.view.setTranslationY((pushing ? offset : -offset) * (1f - progress)); @@ -179,9 +190,16 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV } public void finishTransition(boolean finished) { + if (!transitioning) { + throw new IllegalStateException("finishTransition called when not transitioning"); + } + if (finished) { if (fromItem != null) { - removeNavigationItem(fromItem); + // From a search otherwise + if (fromItem != toItem) { + removeNavigationItem(fromItem); + } } setArrowMenuProgress(toItem.hasBack || toItem.search ? 1f : 0f); } else { @@ -191,6 +209,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV } fromItem = null; + transitioning = false; } public void setCallback(ToolbarCallback callback) { @@ -212,14 +231,6 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV return arrowMenuDrawable; } - @Override - public void onLoadViewRemoved(View view) { - // Remove the menu from the navigation item - if (view instanceof ViewGroup) { - ((ViewGroup) view).removeAllViews(); - } - } - void setTitle(NavigationItem navigationItem) { if (navigationItem.view != null) { TextView titleView = (TextView) navigationItem.view.findViewById(R.id.title); @@ -254,7 +265,6 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(getResources().getDimensionPixelSize(R.dimen.toolbar_height), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL)); navigationItemContainer = new LoadView(getContext()); - navigationItemContainer.setListener(this); addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -287,10 +297,25 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV } private void setNavigationItemInternal(boolean animate, final boolean pushing, NavigationItem newItem) { + if (transitioning) { + throw new IllegalStateException("setNavigationItemInternal called when already transitioning"); + } + attachNavigationItem(newItem); + transitioning = true; + 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(toItem.view, animate); animateArrow(toItem.hasBack || toItem.search); @@ -327,6 +352,10 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV } private void attachNavigationItem(NavigationItem newItem) { + if (transitioning) { + throw new IllegalStateException("attachNavigationItem called while transitioning"); + } + fromItem = toItem; toItem = newItem; @@ -344,6 +373,10 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV } private void removeNavigationItem(NavigationItem item) { + if (!transitioning) { + throw new IllegalStateException("removeNavigationItem called while not transitioning"); + } + item.view.removeAllViews(); navigationItemContainer.removeView(item.view); item.view = null;