org.floens.chan.controller.ui.NavigationControllerContainerLayout.java Source code

Java tutorial

Introduction

Here is the source code for org.floens.chan.controller.ui.NavigationControllerContainerLayout.java

Source

/*
 * 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 <http://www.gnu.org/licenses/>.
 */
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;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.widget.FrameLayout;
import android.widget.Scroller;

import org.floens.chan.controller.Controller;
import org.floens.chan.controller.NavigationController;

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;

    // 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;

    // 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 int shadowPosition;

    public NavigationControllerContainerLayout(Context context) {
        super(context);
        init();
    }

    public NavigationControllerContainerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public NavigationControllerContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
        slopPixels = viewConfiguration.getScaledTouchSlop();
        minimalMovedPixels = dp(3);
        flingPixels = viewConfiguration.getScaledMinimumFlingVelocity();
        maxFlingPixels = viewConfiguration.getScaledMaximumFlingVelocity();

        scroller = new Scroller(getContext());

        shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }

    public void setSwipeEnabled(boolean swipeEnabled) {
        this.swipeEnabled = swipeEnabled;
    }

    public void setNavigationController(NavigationController navigationController) {
        this.navigationController = navigationController;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!swipeEnabled || tracking || navigationController.isBlockingInput()
                || !navigationController.getTop().navigationItem.swipeable || getBelowTop() == null) {
            return false;
        }

        int actionMasked = event.getActionMasked();

        if (actionMasked != MotionEvent.ACTION_DOWN && interceptedEvent == null) {
            // Action down wasn't called here, ignore
            return false;
        }

        switch (actionMasked) {
        case MotionEvent.ACTION_DOWN:
            //                Logger.test("onInterceptTouchEvent down");
            interceptedEvent = MotionEvent.obtain(event);
            break;
        case MotionEvent.ACTION_MOVE: {
            //                Logger.test("onInterceptTouchEvent move");
            float x = (event.getX() - interceptedEvent.getX());
            float y = (event.getY() - interceptedEvent.getY());

            if (Math.abs(y) >= slopPixels || interceptedEvent.getX() < dp(20)) {
                //                    Logger.test("blockTracking = true");
                blockTracking = true;
            }

            if (!blockTracking && x >= minimalMovedPixels && Math.abs(x) > Math.abs(y)) {
                startTracking(event);

                return true;
            }
            break;
        }
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            //                Logger.test("onInterceptTouchEvent cancel/up");
            interceptedEvent.recycle();
            interceptedEvent = null;
            blockTracking = false;
            break;
        }
        }

        return false;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept) {
            if (interceptedEvent != null) {
                interceptedEvent.recycle();
                interceptedEvent = null;
            }
            blockTracking = false;
            if (tracking) {
                endTracking(false);
            }
        }

        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        //        endTracking();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!tracking) {
            return false;
        }

        int translationX = Math.max(0, ((int) event.getX()) - trackStartPosition);
        setTopControllerTranslation(translationX);

        velocityTracker.addMovement(event);

        switch (event.getActionMasked()) {
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            //                Logger.test("onTouchEvent cancel or up");

            scroller.forceFinished(true);

            velocityTracker.addMovement(event);
            velocityTracker.computeCurrentVelocity(1000);
            int velocity = (int) velocityTracker.getXVelocity();

            if (translationX > 0) {
                boolean doFlingAway = false;

                if ((velocity > 0 && Math.abs(velocity) > dp(800) && Math.abs(velocity) < maxFlingPixels)
                        || translationX >= getWidth() * 3 / 4) {
                    //                        int left = getWidth() - translationX;
                    //                        int flingVelocity = Math.max(velocity, 0);

                    //                        Logger.test("flinging with velocity = %d", velocity);
                    velocity = Math.max(dp(2000), velocity);

                    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 (doFlingAway) {
                    startFlingAnimation(true);
                } else {
                    //                        Logger.test("Snapping back");
                    scroller.forceFinished(true);
                    scroller.startScroll(translationX, 0, -translationX, 0, 250);
                    startFlingAnimation(false);
                }
            } else {
                // User swiped back to the left
                endTracking(false);
            }

            velocityTracker.recycle();
            velocityTracker = null;

            break;
        }
        }

        return true;
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        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, shadowPosition, getHeight());
            canvas.drawRect(shadowRect, shadowPaint);
        }
    }

    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);

        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()) {
                float translationX = scroller.getCurrX();

                setTopControllerTranslation((int) translationX);

                // The view is not visible anymore. End it before the fling completely finishes.
                if (translationX >= getWidth()) {
                    finished = true;
                }
            } else {
                finished = true;
            }

            if (!finished) {
                ViewCompat.postOnAnimation(NavigationControllerContainerLayout.this, flingRunnable);
            } else {
                endTracking(finishTransitionAfterAnimation);
            }
        }
    };

    private void setTopControllerTranslation(int translationX) {
        shadowPosition = translationX;
        trackingController.view.setTranslationX(translationX);
        navigationController.swipeTransitionProgress(translationX / (float) getWidth());
        invalidate();
    }

    private Controller getBelowTop() {
        if (navigationController.childControllers.size() >= 2) {
            return navigationController.childControllers.get(navigationController.childControllers.size() - 2);
        } else {
            return null;
        }
    }
}