io.sweers.palettehelper.ui.widget.ElasticDragDismissFrameLayout.java Source code

Java tutorial

Introduction

Here is the source code for io.sweers.palettehelper.ui.widget.ElasticDragDismissFrameLayout.java

Source

/*
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sweers.palettehelper.ui.widget;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;

import java.util.ArrayList;
import java.util.List;

import io.sweers.barber.Barber;
import io.sweers.barber.Kind;
import io.sweers.barber.StyledAttr;
import io.sweers.palettehelper.R;
import io.sweers.palettehelper.util.ColorUtilKt;

/**
 * A {@link FrameLayout} which responds to nested scrolls to create drag-dismissable layouts.
 * Applies an elasticity factor to reduce movement as you approach the given dismiss distance.
 * Optionally also scales down content during drag.
 */
public class ElasticDragDismissFrameLayout extends FrameLayout {

    // configurable attribs
    @StyledAttr(value = R.styleable.ElasticDragDismissFrameLayout_dragDismissDistance, kind = Kind.DIMEN)
    protected float dragDismissDistance = Float.MAX_VALUE;

    @StyledAttr(R.styleable.ElasticDragDismissFrameLayout_dragDismissFraction)
    protected float dragDismissFraction = -1f;

    @StyledAttr(R.styleable.ElasticDragDismissFrameLayout_dragElasticity)
    protected float dragElacticity = 0.8f;

    private float dragDismissScale = 1f;
    private boolean shouldScale = false;

    // state
    private float totalDrag;
    private boolean draggingDown = false;
    private boolean draggingUp = false;

    private List<ElasticDragDismissListener> listeners;

    public ElasticDragDismissFrameLayout(Context context) {
        this(context, null, 0, 0);
    }

    public ElasticDragDismissFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0, 0);
    }

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ElasticDragDismissFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        Barber.style(this, attrs, R.styleable.ElasticDragDismissFrameLayout);
    }

    public interface ElasticDragDismissListener {

        /**
         * Called for each drag event.
         *
         * @param elasticOffset       Indicating the drag offset with elasticity applied i.e. may
         *                            exceed 1.
         * @param elasticOffsetPixels The elastically scaled drag distance in pixels.
         * @param rawOffset           Value from [0, 1] indicating the raw drag offset i.e.
         *                            without elasticity applied. A value of 1 indicates that the
         *                            dismiss distance has been reached.
         * @param rawOffsetPixels     The raw distance the user has dragged
         */
        void onDrag(float elasticOffset, float elasticOffsetPixels, float rawOffset, float rawOffsetPixels);

        /**
         * Called when dragging is released and has exceeded the threshold dismiss distance.
         */
        void onDragDismissed();

    }

    @StyledAttr(R.styleable.ElasticDragDismissFrameLayout_dragDismissScale)
    public void setDragDismissScale(float dragDismissScale) {
        this.dragDismissScale = dragDismissScale;
        this.shouldScale = dragDismissScale != 1f;
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        // if we're in a drag gesture and the user reverses up the we should take those events
        if (draggingDown && dy > 0 || draggingUp && dy < 0) {
            dragScale(dy);
            consumed[1] = dy;
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        dragScale(dyUnconsumed);
    }

    @Override
    public void onStopNestedScroll(View child) {
        if (Math.abs(totalDrag) >= dragDismissDistance) {
            dispatchDismissCallback();
        } else { // settle back to natural position
            animate().translationY(0f).scaleX(1f).scaleY(1f).setDuration(200L)
                    .setInterpolator(new FastOutSlowInInterpolator()).setListener(null).start();
            totalDrag = 0;
            draggingDown = draggingUp = false;
            dispatchDragCallback(0f, 0f, 0f, 0f);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (dragDismissFraction > 0f) {
            dragDismissDistance = h * dragDismissFraction;
        }
    }

    public void addListener(ElasticDragDismissListener listener) {
        if (listeners == null) {
            listeners = new ArrayList<>();
        }
        listeners.add(listener);
    }

    public void removeListener(ElasticDragDismissListener listener) {
        if (listeners != null && listeners.size() > 0) {
            listeners.remove(listener);
        }
    }

    private void dragScale(int scroll) {
        if (scroll == 0) {
            return;
        }

        totalDrag += scroll;

        // track the direction & set the pivot point for scaling
        // don't double track i.e. if start dragging down and then reverse, keep tracking as
        // dragging down until they reach the 'natural' position
        if (scroll < 0 && !draggingUp && !draggingDown) {
            draggingDown = true;
            if (shouldScale) {
                setPivotY(getHeight());
            }
        } else if (scroll > 0 && !draggingDown && !draggingUp) {
            draggingUp = true;
            if (shouldScale) {
                setPivotY(0f);
            }
        }
        // how far have we dragged relative to the distance to perform a dismiss
        // (01 where 1 = dismiss distance). Decreasing logarithmically as we approach the limit
        float dragFraction = (float) Math.log10(1 + (Math.abs(totalDrag) / dragDismissDistance));

        // calculate the desired translation given the drag fraction
        float dragTo = dragFraction * dragDismissDistance * dragElacticity;

        if (draggingUp) {
            // as we use the absolute magnitude when calculating the drag fraction, need to
            // re-apply the drag direction
            dragTo *= -1;
        }
        setTranslationY(dragTo);

        if (shouldScale) {
            final float scale = 1 - ((1 - dragDismissScale) * dragFraction);
            setScaleX(scale);
            setScaleY(scale);
        }

        // if we've reversed direction and gone past the settle point then clear the flags to
        // allow the list to get the scroll events & reset any transforms
        if ((draggingDown && totalDrag >= 0) || (draggingUp && totalDrag <= 0)) {
            totalDrag = dragTo = dragFraction = 0;
            draggingDown = draggingUp = false;
            setTranslationY(0f);
            setScaleX(1f);
            setScaleY(1f);
        }
        dispatchDragCallback(dragFraction, dragTo, Math.min(1f, Math.abs(totalDrag) / dragDismissDistance),
                totalDrag);
    }

    private void dispatchDragCallback(float elasticOffset, float elasticOffsetPixels, float rawOffset,
            float rawOffsetPixels) {
        if (listeners != null && listeners.size() > 0) {
            for (ElasticDragDismissListener listener : listeners) {
                listener.onDrag(elasticOffset, elasticOffsetPixels, rawOffset, rawOffsetPixels);
            }
        }
    }

    private void dispatchDismissCallback() {
        if (listeners != null && listeners.size() > 0) {
            for (ElasticDragDismissListener listener : listeners) {
                listener.onDragDismissed();
            }
        }
    }

    /**
     * An {@link ElasticDragDismissListener} which fades system chrome (i.e. status bar and
     * navigation bar) when elastic drags are performed. Consuming classes must provide the
     * implementation for {@link ElasticDragDismissListener#onDragDismissed()}.
     */
    public static abstract class SystemChromeFader implements ElasticDragDismissListener {

        private Window window;
        private static final boolean SHOULD_OP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;

        public SystemChromeFader(Window window) {
            this.window = window;
        }

        @SuppressLint("NewApi")
        @Override
        public void onDrag(float elasticOffset, float elasticOffsetPixels, float rawOffset, float rawOffsetPixels) {
            if (SHOULD_OP) {
                if (elasticOffsetPixels < 0) {
                    // dragging upward, fade the navigation bar in proportion
                    // TODO don't fade nav bar on landscape phones?
                    window.setNavigationBarColor(
                            ColorUtilKt.modifyAlpha(window.getNavigationBarColor(), 1f - rawOffset));
                } else if (elasticOffsetPixels == 0) {
                    // reset
                    window.setStatusBarColor(ColorUtilKt.modifyAlpha(window.getStatusBarColor(), 1f));
                    window.setNavigationBarColor(ColorUtilKt.modifyAlpha(window.getNavigationBarColor(), 1f));
                } else {
                    // dragging downward, fade the status bar in proportion
                    window.setStatusBarColor(ColorUtilKt.modifyAlpha(window.getStatusBarColor(), 1f - rawOffset));
                }
            }
        }

        public abstract void onDragDismissed();
    }

}