From 81996038462de1be86643e95d262933c4b96c551 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 8 Jul 2017 20:25:23 -0700 Subject: Move components to separate modules --- .../ui/widgets/ElasticDragDismissFrameLayout.kt | 237 +++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 core-ui/src/main/kotlin/ca/allanwang/kau/ui/widgets/ElasticDragDismissFrameLayout.kt (limited to 'core-ui/src/main/kotlin/ca/allanwang/kau/ui/widgets/ElasticDragDismissFrameLayout.kt') diff --git a/core-ui/src/main/kotlin/ca/allanwang/kau/ui/widgets/ElasticDragDismissFrameLayout.kt b/core-ui/src/main/kotlin/ca/allanwang/kau/ui/widgets/ElasticDragDismissFrameLayout.kt new file mode 100644 index 0000000..452fd56 --- /dev/null +++ b/core-ui/src/main/kotlin/ca/allanwang/kau/ui/widgets/ElasticDragDismissFrameLayout.kt @@ -0,0 +1,237 @@ +/* + * 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 ca.allanwang.kau.ui.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import ca.allanwang.kau.R +import ca.allanwang.kau.utils.* + +/** + * A [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. + */ +class ElasticDragDismissFrameLayout @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + + // configurable attribs + var dragDismissDistance = context.dimen(R.dimen.kau_drag_dismiss_distance).dpToPx + var dragDismissFraction = -1f + var dragDismissScale = 1f + set(value) { + field = value + shouldScale = value != 1f + } + private var shouldScale = false + var dragElacticity = 0.8f + + // state + private var totalDrag: Float = 0f + private var draggingDown = false + private var draggingUp = false + + private var callbacks: MutableList = mutableListOf() + + init { + if (attrs != null) { + val a = getContext().obtainStyledAttributes(attrs, R.styleable.ElasticDragDismissFrameLayout, 0, 0) + dragDismissDistance = a.getDimensionPixelSize(R.styleable.ElasticDragDismissFrameLayout_dragDismissDistance, Int.MAX_VALUE).toFloat() + dragDismissFraction = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissFraction, dragDismissFraction) + dragDismissScale = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissScale, dragDismissScale) + dragElacticity = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragElasticity, dragElacticity) + a.recycle() + } + } + + 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 + */ + internal open fun onDrag(elasticOffset: Float, elasticOffsetPixels: Float, + rawOffset: Float, rawOffsetPixels: Float) { + } + + /** + * Called when dragging is released and has exceeded the threshold dismiss distance. + */ + internal open fun onDragDismissed() {} + + } + + override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean { + return nestedScrollAxes and View.SCROLL_AXIS_VERTICAL != 0 + } + + override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) { + // 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 fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, + dxUnconsumed: Int, dyUnconsumed: Int) { + dragScale(dyUnconsumed) + } + + override fun onStopNestedScroll(child: View) { + if (Math.abs(totalDrag) >= dragDismissDistance) { + dispatchDismissCallback() + } else { // settle back to natural position + animate() + .translationY(0f) + .scaleX(1f) + .scaleY(1f) + .setDuration(200L) + .setInterpolator(AnimHolder.fastOutSlowInInterpolator(context)) + .setListener(null) + .start() + totalDrag = 0f + draggingUp = false + draggingDown = draggingUp + dispatchDragCallback(0f, 0f, 0f, 0f) + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (dragDismissFraction > 0f) { + dragDismissDistance = h * dragDismissFraction + } + } + + fun addListener(listener: ElasticDragDismissCallback) { + callbacks.add(listener) + } + + fun removeListener(listener: ElasticDragDismissCallback) { + callbacks.remove(listener) + } + + private fun dragScale(scroll: Int) { + if (scroll == 0) return + + totalDrag += scroll.toFloat() + + // 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) pivotY = height.toFloat() + } else if (scroll > 0 && !draggingDown && !draggingUp) { + draggingUp = true + if (shouldScale) pivotY = 0f + } + // how far have we dragged relative to the distance to perform a dismiss + // (0–1 where 1 = dismiss distance). Decreasing logarithmically as we approach the limit + var dragFraction = Math.log10((1 + Math.abs(totalDrag) / dragDismissDistance).toDouble()).toFloat() + + // calculate the desired translation given the drag fraction + var 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 *= -1f + } + translationY = dragTo + + if (shouldScale) { + val scale = 1 - (1 - dragDismissScale) * dragFraction + scaleX = scale + scaleY = 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) { + dragFraction = 0f + dragTo = dragFraction + totalDrag = dragTo + draggingUp = false + draggingDown = draggingUp + translationY = 0f + scaleX = 1f + scaleY = 1f + } + dispatchDragCallback(dragFraction, dragTo, + Math.min(1f, Math.abs(totalDrag) / dragDismissDistance), totalDrag) + } + + private fun dispatchDragCallback(elasticOffset: Float, elasticOffsetPixels: Float, + rawOffset: Float, rawOffsetPixels: Float) { + callbacks.forEach { + it.onDrag(elasticOffset, elasticOffsetPixels, + rawOffset, rawOffsetPixels) + } + } + + private fun dispatchDismissCallback() { + callbacks.forEach { it.onDragDismissed() } + } + + /** + * An [ElasticDragDismissCallback] which fades system chrome (i.e. status bar and + * navigation bar) whilst elastic drags are performed and + * [finishes][Activity.finishAfterTransition] the activity when drag dismissed. + */ + open class SystemChromeFader(private val activity: Activity) : ElasticDragDismissCallback() { + private val statusBarAlpha: Int = Color.alpha(activity.statusBarColor) + private val navBarAlpha: Int = Color.alpha(activity.navigationBarColor) + private val fadeNavBar: Boolean = activity.isNavBarOnBottom + + public override fun onDrag(elasticOffset: Float, elasticOffsetPixels: Float, + rawOffset: Float, rawOffsetPixels: Float) { + if (elasticOffsetPixels > 0) { + // dragging downward, fade the status bar in proportion + activity.statusBarColor = activity.statusBarColor.withAlpha(((1f - rawOffset) * statusBarAlpha).toInt()) + } else if (elasticOffsetPixels == 0f) { + // reset + activity.statusBarColor = activity.statusBarColor.withAlpha(statusBarAlpha) + activity.navigationBarColor = activity.navigationBarColor.withAlpha(navBarAlpha) + } else if (fadeNavBar) { + // dragging upward, fade the navigation bar in proportion + activity.navigationBarColor = activity.navigationBarColor.withAlpha(((1f - rawOffset) * navBarAlpha).toInt()) + } + } + + public override fun onDragDismissed() { + activity.finishAfterTransition() + } + } + +} -- cgit v1.2.3