Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/util/view/AppBarChildBehavior.kt

354 lines
15 KiB
Kotlin

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 org.mariotaku.twidere.util.view
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Rect
import androidx.annotation.Keep
import androidx.annotation.StyleableRes
import com.google.android.material.appbar.AppBarLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.widget.TextView
import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.*
/**
* Created by mariotaku on 2017/2/20.
*/
class AppBarChildBehavior(
context: Context,
attrs: AttributeSet? = null
) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val appBarId: Int
private val toolbarId: Int
private val dependencyViewId: Int
private val targetViewId: Int
private val alignmentRule: Int
private val marginTop: Int
private val marginBottom: Int
private val marginLeft: Int
private val marginRight: Int
private val marginStart: Int
private val marginEnd: Int
private val transformation: ChildTransformation
private val dependencyRect = Rect()
private val layoutRect = Rect()
private val thisRect = Rect()
private val targetRect = Rect()
private val tempLocation = IntArray(2)
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.AppBarChildBehavior)
appBarId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_appBarId, "appBarId")
toolbarId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_toolbarId, "toolbarId")
dependencyViewId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_dependencyViewId, "dependencyViewId")
targetViewId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_targetViewId, "targetViewId")
alignmentRule = a.getIntegerOrThrow(R.styleable.AppBarChildBehavior_behavior_alignmentRule, "alignmentRule")
marginTop = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginTop, 0)
marginBottom = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginBottom, 0)
marginLeft = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginLeft, 0)
marginRight = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginRight, 0)
marginStart = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginStart, 0)
marginEnd = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginEnd, 0)
transformation = a.getTransformation(R.styleable.AppBarChildBehavior_behavior_childTransformation)
a.recycle()
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
return dependency.id == dependencyViewId
}
override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean {
val target = parent.findViewById<View>(targetViewId)
val dependency = parent.getDependencies(child).first()
dependency.getFrameRelatedTo(dependencyRect, parent)
layoutRect.layoutRelatedTo(child, parent, dependencyRect, layoutDirection)
child.layout(layoutRect.left, layoutRect.top, layoutRect.right, layoutRect.bottom)
child.getFrameRelatedTo(thisRect, parent)
target.getFrameRelatedTo(targetRect, parent)
transformation.onChildLayoutChanged(child, dependency, target)
return true
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
val appBar = parent.findViewById<View>(appBarId)
val target = parent.findViewById<View>(targetViewId)
val toolbar = parent.findViewById<View>(toolbarId)
val behavior = (appBar.layoutParams as CoordinatorLayout.LayoutParams).behavior as AppBarLayout.Behavior
toolbar.getLocationOnScreen(tempLocation)
val offset = behavior.topAndBottomOffset
val percent = offset / (tempLocation[1] + toolbar.height - appBar.height).toFloat()
transformation.onTargetChanged(child, thisRect, target, targetRect, percent, offset)
return true
}
internal fun Rect.layoutRelatedTo(view: View, parent: CoordinatorLayout, frame: Rect, layoutDirection: Int) {
val verticalRule = alignmentRule and VERTICAL_MASK
val horizontalRule = alignmentRule and HORIZONTAL_MASK
set(0, 0, view.measuredWidth, view.measuredHeight)
when (verticalRule) {
ALIGNMENT_CENTER_VERTICAL -> {
offsetTopTo(frame.centerY() - view.measuredHeight / 2 + marginTop - marginBottom)
}
0, ALIGNMENT_TOP -> {
offsetTopTo(frame.top + marginTop)
}
ALIGNMENT_BOTTOM -> {
offsetBottomTo(frame.bottom - marginBottom)
}
ALIGNMENT_ABOVE -> {
offsetBottomTo(frame.top + marginTop - marginBottom)
}
ALIGNMENT_BELOW -> {
offsetTopTo(frame.bottom + marginTop - marginBottom)
}
ALIGNMENT_ABOVE_CENTER -> {
offsetBottomTo(frame.centerY() + marginTop - marginBottom)
}
ALIGNMENT_BELOW_CENTER -> {
offsetTopTo(frame.centerY() + marginTop - marginBottom)
}
else -> {
throw IllegalArgumentException("Illegal alignment flag ${Integer.toHexString(alignmentRule)}")
}
}
when (horizontalRule) {
ALIGNMENT_CENTER_HORIZONTAL -> {
offsetLeftTo(frame.centerX() - view.measuredWidth / 2
+ absoluteMarginLeft(layoutDirection) - absoluteMarginRight(layoutDirection))
}
0, ALIGNMENT_LEFT -> {
offsetLeftTo(frame.left + absoluteMarginLeft(layoutDirection))
}
ALIGNMENT_RIGHT -> {
offsetRightTo(frame.right - absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_LEFT_OF -> {
offsetRightTo(frame.left + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_RIGHT_OF -> {
offsetLeftTo(frame.right + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_LEFT_OF_CENTER -> {
offsetRightTo(frame.centerX() + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_RIGHT_OF_CENTER -> {
offsetLeftTo(frame.centerX() + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_START -> {
offsetStartTo(frame.getStart(layoutDirection)
+ relativeMarginStart(layoutDirection), layoutDirection)
}
ALIGNMENT_END -> {
offsetEndTo(frame.getEnd(layoutDirection)
- relativeMarginEnd(layoutDirection), layoutDirection)
}
ALIGNMENT_TO_START_OF -> {
offsetEndTo(frame.getStart(layoutDirection)
+ relativeMarginStart(layoutDirection) - relativeMarginEnd(layoutDirection),
layoutDirection)
}
ALIGNMENT_TO_END_OF -> {
offsetStartTo(frame.getEnd(layoutDirection)
+ relativeMarginStart(layoutDirection) - relativeMarginEnd(layoutDirection),
layoutDirection)
}
ALIGNMENT_TO_START_OF_CENTER -> {
offsetEndTo(frame.centerX() + relativeMarginStart(layoutDirection)
- relativeMarginEnd(layoutDirection), layoutDirection)
}
ALIGNMENT_TO_END_OF_CENTER -> {
offsetStartTo(frame.centerX() + relativeMarginStart(layoutDirection)
- relativeMarginEnd(layoutDirection), layoutDirection)
}
else -> {
throw IllegalArgumentException("Illegal alignment flag ${Integer.toHexString(alignmentRule)}")
}
}
left = left.coerceAtLeast(absoluteMarginLeft(layoutDirection) - frame.left)
right = right.coerceAtMost(parent.measuredWidth - absoluteMarginRight(layoutDirection) - frame.left)
// tempRect.top = tempRect.top.coerceAtLeast(marginTop - frame.top)
// tempRect.bottom = tempRect.bottom.coerceAtLeast(parent.measuredHeight - marginBottom - frame.top)
}
private fun absoluteMarginLeft(layoutDirection: Int): Int {
if (marginStart == 0 && marginEnd == 0) return marginLeft
return if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
marginEnd
} else {
marginStart
}
}
private fun absoluteMarginRight(layoutDirection: Int): Int {
if (marginStart == 0 && marginEnd == 0) return marginRight
return if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
marginStart
} else {
marginEnd
}
}
private fun relativeMarginStart(layoutDirection: Int): Int {
return if (marginStart != 0) {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
-marginStart
} else {
marginStart
}
} else {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
marginRight
} else {
marginLeft
}
}
}
private fun relativeMarginEnd(layoutDirection: Int): Int {
return if (marginEnd != 0) {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
-marginEnd
} else {
marginEnd
}
} else {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
marginLeft
} else {
marginRight
}
}
}
interface ChildTransformation {
fun onChildLayoutChanged(child: View, dependency: View, target: View) {}
fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int)
}
open class ScaleTransformation : ChildTransformation {
override fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int) {
child.pivotX = child.width.toFloat()
child.pivotY = child.height.toFloat()
child.scaleX = 1 - (frame.width() - targetFrame.width()) * percent / frame.width()
child.scaleY = 1 - (frame.height() - targetFrame.height()) * percent / frame.height()
child.translationX = (targetFrame.right - frame.right) * percent
child.translationY = -offset - (frame.bottom - offset - targetFrame.bottom) * percent
}
}
@Keep
open class TextViewTransformation : ChildTransformation {
private var sourceSize: Float = Float.NaN
private var destSize: Float = Float.NaN
private var viewLaidOut: Boolean = false
override fun onChildLayoutChanged(child: View, dependency: View, target: View) {
if (viewLaidOut) return
child as TextView
target as TextView
sourceSize = child.textSize
destSize = target.textSize
viewLaidOut = true
}
override fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int) {
child as TextView
child.pivotX = child.width.toFloat()
child.pivotY = child.height.toFloat()
child.translationX = (targetFrame.left - frame.left) * percent
child.translationY = -offset - (frame.bottom - offset - targetFrame.bottom) * percent
child.setTextSize(TypedValue.COMPLEX_UNIT_PX, sourceSize + (destSize - sourceSize) * percent)
}
}
companion object {
private const val HORIZONTAL_MASK: Int = 0x0000FFFF
private const val VERTICAL_MASK: Int = 0xFFFF0000.toInt()
private const val HORIZONTAL_RELATIVE_FLAG: Int = 0x00001000
const val ALIGNMENT_LEFT: Int = 0x00000001
const val ALIGNMENT_RIGHT: Int = 0x00000002
const val ALIGNMENT_TO_LEFT_OF: Int = 0x00000004
const val ALIGNMENT_TO_RIGHT_OF: Int = 0x00000008
const val ALIGNMENT_TO_LEFT_OF_CENTER: Int = 0x00000010
const val ALIGNMENT_TO_RIGHT_OF_CENTER: Int = 0x00000020
const val ALIGNMENT_CENTER_HORIZONTAL: Int = ALIGNMENT_LEFT or ALIGNMENT_RIGHT
const val ALIGNMENT_START: Int = ALIGNMENT_LEFT or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_END: Int = ALIGNMENT_RIGHT or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_START_OF: Int = ALIGNMENT_TO_LEFT_OF or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_END_OF: Int = ALIGNMENT_TO_RIGHT_OF or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_START_OF_CENTER: Int = ALIGNMENT_TO_LEFT_OF_CENTER or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_END_OF_CENTER: Int = ALIGNMENT_TO_RIGHT_OF_CENTER or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TOP: Int = 0x00010000
const val ALIGNMENT_BOTTOM: Int = 0x00020000
const val ALIGNMENT_ABOVE: Int = 0x00040000
const val ALIGNMENT_BELOW: Int = 0x00080000
const val ALIGNMENT_ABOVE_CENTER: Int = 0x00100000
const val ALIGNMENT_BELOW_CENTER: Int = 0x00200000
const val ALIGNMENT_CENTER_VERTICAL: Int = ALIGNMENT_TOP or ALIGNMENT_BOTTOM
private fun TypedArray.getIntegerOrThrow(@StyleableRes index: Int, name: String): Int {
if (!hasValue(index)) {
throw IllegalArgumentException("$name required")
}
return getInteger(index, 0)
}
private fun TypedArray.getResourceIdOrThrow(@StyleableRes index: Int, name: String): Int {
if (!hasValue(index)) {
throw IllegalArgumentException("$name required")
}
return getResourceId(index, 0)
}
private fun TypedArray.getTransformation(@StyleableRes index: Int): ChildTransformation {
val className = getString(index) ?: return ScaleTransformation()
return Class.forName(className).newInstance() as ChildTransformation
}
}
}