SlidingLayout.java 9.96 KB
package etelligens.com.foodsafety.utils;

import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;
import android.widget.Scroller;

public class SlidingLayout extends LinearLayout {

    // Duration of sliding animation, in miliseconds
    private static final int SLIDING_DURATION = 400;

    // Query Scroller every 16 miliseconds
    private static final int QUERY_INTERVAL = 16;

    // Sliding width
    int sldingLayoutWidth;

    // Sliding menu
    private View menu;

    // Main content
    private View content;

    // menu does not occupy some right space
    // This should be updated correctly later in onMeasure
    private static int menuRightMargin = 0;

    // The state of menu
    private enum MenuState {
        HIDING,
        HIDDEN,
        SHOWING,
        SHOWN,
    }

    // content will be layouted based on this X offset
    // Normally, contentXOffset = menu.getLayoutParams().width = this.getWidth - menuRightMargin
    private int contentXOffset;

    // menu is hidden when initializing
    private MenuState currentMenuState = MenuState.HIDDEN;

    // Scroller is used to facilitate animation
    private Scroller menuScroller = new Scroller(this.getContext(),
            new EaseInInterpolator());

    // Used to query Scroller about scrolling position
    // Note: The 3rd paramter to startScroll is the distance
    private Runnable menuRunnable = new MenuRunnable();
    private Handler menuHandler = new Handler();

    // Previous touch position
    int prevX = 0;

    // Is user dragging the content
    boolean isDragging = false;

    // Used to facilitate ACTION_UP
    int lastDiffX = 0;


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

    public SlidingLayout(Context context) {
        super(context);
    }

    // Overriding LinearLayout core methods
    // Ask all children to measure themselves and compute the measurement of this
    // layout based on the children
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        sldingLayoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        //Sliding menu will take 85% screen size when completely shown
        menuRightMargin = sldingLayoutWidth * 30/ 100;
    }

    // This is called when SlidingLayout is attached to window
    // At this point it has a Surface and will start drawing.
    // Note that this function is guaranteed to be called before onDraw
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        // Get our 2 children views
        menu = this.getChildAt(0);
        content = this.getChildAt(1);

        // Attach View.OnTouchListener
        content.setOnTouchListener((v, event) -> onContentTouch(v, event));

        menu.setVisibility(View.GONE);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // True if SlidingLayout's size and position has changed
        // If true, calculate child views size
        if (changed) {
            // Note: LayoutParams are used by views to tell their parents how they want to be laid out
            // content View occupies the full height and width
            LayoutParams contentLayoutParams = (LayoutParams) content.getLayoutParams();
            contentLayoutParams.height = this.getHeight();
            contentLayoutParams.width = this.getWidth();

            // menu View occupies the full height, but certain width
            LayoutParams menuLayoutParams = (LayoutParams) menu.getLayoutParams();
            menuLayoutParams.height = this.getHeight();
            menuLayoutParams.width = this.getWidth() - menuRightMargin;
        }

        // Layout the child views
        menu.layout(left, top, right - menuRightMargin, bottom);
        content.layout(left + contentXOffset, top, right + contentXOffset, bottom);
    }

    // Custom methods for SlidingLayout
    // Used to show/hide menu accordingly
    public void toggleMenu() {
        // Do nothing if sliding is in progress
        if (currentMenuState == MenuState.HIDING || currentMenuState == MenuState.SHOWING)
            return;

        switch (currentMenuState) {
            case HIDDEN:
                currentMenuState = MenuState.SHOWING;
                menu.setVisibility(View.VISIBLE);
                menuScroller.startScroll(0, 0, menu.getLayoutParams().width,
                        0, SLIDING_DURATION);
                break;
            case SHOWN:
                currentMenuState = MenuState.HIDING;
                menuScroller.startScroll(contentXOffset, 0, -contentXOffset,
                        0, SLIDING_DURATION);
                break;
            default:
                break;
        }

        // Begin querying
        menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);

        // Invalite this whole SlidingLayout, causing onLayout() to be called
        this.invalidate();
    }

    // Query Scroller
    protected class MenuRunnable implements Runnable {
        @Override
        public void run() {
            boolean isScrolling = menuScroller.computeScrollOffset();
            adjustContentPosition(isScrolling);
        }
    }

    // Adjust content View position to match sliding animation
    private void adjustContentPosition(boolean isScrolling) {
        int scrollerXOffset = menuScroller.getCurrX();

        // Translate content View accordingly
        content.offsetLeftAndRight(scrollerXOffset - contentXOffset);

        contentXOffset = scrollerXOffset;

        // Invalite this whole Slidinglayout, causing onLayout() to be called
        this.invalidate();

        // Check if animation is in progress
        if (isScrolling)
            menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);
        else
            this.onMenuSlidingComplete();
    }

    // Called when sliding is complete
    private void onMenuSlidingComplete() {
        switch (currentMenuState) {
            case SHOWING:
                currentMenuState = MenuState.SHOWN;
                break;
            case HIDING:
                currentMenuState = MenuState.HIDDEN;
                menu.setVisibility(View.GONE);
                break;
            default:
                return;
        }
    }

    // Make scrolling more natural. Move more quickly at the end
    protected class EaseInInterpolator implements Interpolator {
        @Override
        public float getInterpolation(float t) {
            return (float) Math.pow(t - 1, 5) + 1;
        }
    }

    // Is menu completely shown
    public boolean isMenuShown() {
        return currentMenuState == MenuState.SHOWN;
    }

    // Handle touch event on content View
    public boolean onContentTouch(View v, MotionEvent event) {
        // Do nothing if sliding is in progress
        if (currentMenuState == MenuState.HIDING || currentMenuState == MenuState.SHOWING)
            return false;

        // getRawX returns X touch point corresponding to screen
        // getX sometimes returns screen X, sometimes returns content View X
        int curX = (int) event.getRawX();
        int diffX = 0;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                prevX = curX;
                return true;

            case MotionEvent.ACTION_MOVE:
                // Set menu to Visible when user start dragging the content View
                if (!isDragging) {
                    isDragging = true;
                    menu.setVisibility(View.VISIBLE);
                }

                // How far we have moved since the last position
                diffX = curX - prevX;

                // Prevent user from dragging beyond border
                if (contentXOffset + diffX <= 0) {
                    // Don't allow dragging beyond left border
                    // Use diffX will make content cross the border, so only translate by -contentXOffset
                    diffX = -contentXOffset;
                } else if (contentXOffset + diffX > sldingLayoutWidth - menuRightMargin) {
                    // Don't allow dragging beyond menu width
                    diffX = sldingLayoutWidth - menuRightMargin - contentXOffset;
                }

                // Translate content View accordingly
                content.offsetLeftAndRight(diffX);

                contentXOffset += diffX;

                // Invalite this whole Slidinglayout, causing onLayout() to be called
                this.invalidate();

                prevX = curX;
                lastDiffX = diffX;
                return true;

            case MotionEvent.ACTION_UP:
                // Start scrolling
                // Remember that when content has a chance to cross left border, lastDiffX is set to 0
                if (lastDiffX > 0) {
                    // User wants to show menu
                    currentMenuState = MenuState.SHOWING;

                    // Start scrolling from contentXOffset
                    menuScroller.startScroll(contentXOffset, 0, menu.getLayoutParams().width - contentXOffset,
                            0, SLIDING_DURATION);
                } else if (lastDiffX < 0) {
                    // User wants to hide menu
                    currentMenuState = MenuState.HIDING;
                    menuScroller.startScroll(contentXOffset, 0, -contentXOffset,
                            0, SLIDING_DURATION);
                }

                // Begin querying
                menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);

                // Invalite this whole Slidinglayout, causing onLayout() to be called
                this.invalidate();

                // Done dragging
                isDragging = false;
                prevX = 0;
                lastDiffX = 0;
                return true;

            default:
                break;
        }
        return false;
    }
}