arun.com.chromer.browsing.article.view.ElasticDragDismissFrameLayout.java Source code

Java tutorial

Introduction

Here is the source code for arun.com.chromer.browsing.article.view.ElasticDragDismissFrameLayout.java

Source

/*
 * Lynket
 *
 * Copyright (C) 2019 Arunkumar
 *
 * 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 arun.com.chromer.browsing.article.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;

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

import arun.com.chromer.R;

/**
 * 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.
 * <p>
 * Adapted from
 * https://github.com/nickbutcher/plaid/blob/master/app/src/main/java/io/plaidapp/
 * ui/widget/ElasticDragDismissFrameLayout.java
 * with some changes so that the background color can be adjusted behind the dragging view.
 * <p>
 * This view is required to only have a single child.
 */
public class ElasticDragDismissFrameLayout extends FrameLayout {

    private static Interpolator fastOutSlowInInterpolator;
    // configurable attribs
    private float dragDismissDistance = Float.MAX_VALUE;
    private final float dragDismissFraction = -1f;
    private final float dragDismissScale = 1f;
    private boolean shouldScale = false;
    private final float dragElasticity = 0.5f;
    // state
    private float totalDrag;
    private boolean draggingDown = false;
    private boolean draggingUp = false;
    private boolean enabled = true;
    private List<ElasticDragDismissCallback> callbacks;

    private RectF draggingBackground;
    private Paint draggingBackgroundPaint;

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

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

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

    private void init() {
        dragDismissDistance = getResources().getDimensionPixelSize(R.dimen.article_drag_down_dismiss_distance);

        shouldScale = dragDismissScale != 1f;

        draggingBackgroundPaint = new Paint();
        draggingBackgroundPaint
                .setColor(getContext().getResources().getColor(R.color.article_transparentSideBackground));
        draggingBackgroundPaint.setStyle(Paint.Style.FILL);
    }

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

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (enabled) {
            // 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) {
        if (enabled) {
            dragScale(dyUnconsumed);
        }
    }

    @Override
    public void onStopNestedScroll(View child) {
        if (enabled) {
            if (Math.abs(totalDrag) >= dragDismissDistance) {
                dispatchDismissCallback();
            } else { // settle back to natural position
                if (fastOutSlowInInterpolator == null) {
                    fastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
                            android.R.interpolator.fast_out_slow_in);
                }
                getChildAt(0).animate().translationY(0f).scaleX(1f).scaleY(1f).setDuration(200L)
                        .setInterpolator(fastOutSlowInInterpolator).setListener(null).start();

                ValueAnimator animator = null;
                if (draggingUp) {
                    animator = ValueAnimator.ofFloat(draggingBackground.top, draggingBackground.bottom);
                    animator.addUpdateListener(valueAnimator -> {
                        draggingBackground.top = (float) valueAnimator.getAnimatedValue();
                        invalidate();
                    });
                } else if (draggingDown) {
                    animator = ValueAnimator.ofFloat(draggingBackground.bottom, draggingBackground.top);
                    animator.addUpdateListener(valueAnimator -> {
                        draggingBackground.bottom = (float) valueAnimator.getAnimatedValue();
                        invalidate();
                    });
                }

                if (animator != null) {
                    animator.setInterpolator(fastOutSlowInInterpolator);
                    animator.setDuration(200L);
                    animator.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(ElasticDragDismissCallback listener) {
        if (callbacks == null) {
            callbacks = new ArrayList<>();
        }
        callbacks.add(listener);
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

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

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

        totalDrag += scroll;
        View child = getChildAt(0);

        // track the direction & set the pivot point for scaling
        // don't double track i.e. if play 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)
                child.setPivotY(getHeight());
        } else if (scroll > 0 && !draggingDown && !draggingUp) {
            draggingUp = true;
            if (shouldScale)
                child.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 * dragElasticity;

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

        if (draggingBackground == null) {
            draggingBackground = new RectF();
            draggingBackground.left = 0;
            draggingBackground.right = getWidth();
        }

        if (shouldScale) {
            final float scale = 1 - ((1 - dragDismissScale) * dragFraction);
            child.setScaleX(scale);
            child.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;
            child.setTranslationY(0f);
            child.setScaleX(1f);
            child.setScaleY(1f);
        }

        // draw the background above or below the view where it has scrolled at
        if (draggingUp) {
            draggingBackground.bottom = getHeight();
            draggingBackground.top = getHeight() + dragTo;
            invalidate();
        } else if (draggingDown) {
            draggingBackground.top = 0;
            draggingBackground.bottom = dragTo;
            invalidate();
        }

        dispatchDragCallback(dragFraction, dragTo, Math.min(1f, Math.abs(totalDrag) / dragDismissDistance),
                totalDrag);
    }

    private void dispatchDragCallback(float elasticOffset, float elasticOffsetPixels, float rawOffset,
            float rawOffsetPixels) {
        if (callbacks != null && !callbacks.isEmpty()) {
            for (ElasticDragDismissCallback callback : callbacks) {
                callback.onDrag(elasticOffset, elasticOffsetPixels, rawOffset, rawOffsetPixels);
            }
        }
    }

    private void dispatchDismissCallback() {
        if (callbacks != null && !callbacks.isEmpty()) {
            for (ElasticDragDismissCallback callback : callbacks) {
                callback.onDragDismissed();
            }
        }
    }

    public boolean isDragging() {
        return draggingDown || draggingUp;
    }

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

        if (draggingBackground != null) {
            canvas.drawRect(draggingBackground, draggingBackgroundPaint);
        }
    }

    public static abstract class ElasticDragDismissCallback {

        /**
         * 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
         */
        public void onDrag(float elasticOffset, float elasticOffsetPixels, float rawOffset, float rawOffsetPixels) {
        }

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

    }

}