Java tutorial
/* * 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.plaidapp.ui.widget; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.View; import android.view.Window; import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; import java.util.ArrayList; import java.util.List; import io.plaidapp.R; import io.plaidapp.util.ColorUtils; /** * 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 private float dragDismissDistance = Float.MAX_VALUE; private float dragDismissFraction = -1f; private float dragDismissScale = 1f; private boolean shouldScale = false; private float dragElacticity = 0.8f; // 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) { super(context, attrs); } 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) { final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ElasticDragDismissFrameLayout, 0, 0); if (a.hasValue(R.styleable.ElasticDragDismissFrameLayout_dragDismissDistance)) { dragDismissDistance = a .getDimensionPixelSize(R.styleable.ElasticDragDismissFrameLayout_dragDismissDistance, 0); } else if (a.hasValue(R.styleable.ElasticDragDismissFrameLayout_dragDismissFraction)) { dragDismissFraction = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissFraction, dragDismissFraction); } if (a.hasValue(R.styleable.ElasticDragDismissFrameLayout_dragDismissScale)) { dragDismissScale = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissScale, dragDismissScale); shouldScale = dragDismissScale != 1f; } if (a.hasValue(R.styleable.ElasticDragDismissFrameLayout_dragElasticity)) { dragElacticity = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragElasticity, dragElacticity); } a.recycle(); } 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(); } /** * View * @param child * @param target * @param nestedScrollAxes * @return */ @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { // return (nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0; //? 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(AnimationUtils.loadInterpolator(getContext(), android.R // .interpolator.fast_out_slow_in)) .setInterpolator(new OvershootInterpolator()).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 // ???? View??? 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; public SystemChromeFader(Window window) { this.window = window; } @Override public void onDrag(float elasticOffset, float elasticOffsetPixels, float rawOffset, float rawOffsetPixels) { if (elasticOffsetPixels < 0) { // dragging upward, fade the navigation bar in proportion // TODO don't fade nav bar on landscape phones? window.setNavigationBarColor( ColorUtils.modifyAlpha(window.getNavigationBarColor(), 1f - rawOffset)); } else if (elasticOffsetPixels == 0) { // reset window.setStatusBarColor(ColorUtils.modifyAlpha(window.getStatusBarColor(), 1f)); window.setNavigationBarColor(ColorUtils.modifyAlpha(window.getNavigationBarColor(), 1f)); } else { // dragging downward, fade the status bar in proportion window.setStatusBarColor(ColorUtils.modifyAlpha(window.getStatusBarColor(), 1f - rawOffset)); } } public abstract void onDragDismissed(); } }