Skip to content

Commit

Permalink
Added custom predictive back gesture layer (#329)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hamza417 committed Jul 30, 2024
1 parent a6d852a commit 8f01dbf
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 14 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ dependencies {
implementation 'androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.transition:transition-ktx:1.5.1'
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")

// Google
implementation 'com.google.android.material:material:1.12.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package app.simple.inure.decorations.transitions

import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import androidx.transition.Transition
import androidx.transition.TransitionValues

class CustomTransition : Transition() {
override fun captureStartValues(transitionValues: TransitionValues) {
transitionValues.values["custom:transition:alpha"] = transitionValues.view.alpha
}

override fun captureEndValues(transitionValues: TransitionValues) {
transitionValues.values["custom:transition:alpha"] = 1f
}

override fun createAnimator(
sceneRoot: ViewGroup,
startValues: TransitionValues?,
endValues: TransitionValues?
): Animator? {
val view = endValues?.view ?: return null
val startAlpha = startValues?.values?.get("custom:transition:alpha") as? Float ?: 0f
val endAlpha = endValues.values["custom:transition:alpha"] as Float
return ObjectAnimator.ofFloat(view, View.ALPHA, startAlpha, endAlpha)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.io.IOException
import java.io.InputStream

@SuppressLint("SetJavaScriptEnabled")
class XMLWebView(context: Context, attributeSet: AttributeSet) : WebView(context, attributeSet) {
class WebViewXMLViewer(context: Context, attributeSet: AttributeSet) : WebView(context, attributeSet) {
init {
settings.apply {
useWideViewPort = false
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/app/simple/inure/dialogs/app/HtmlViewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import app.simple.inure.R
import app.simple.inure.decorations.views.XMLWebView
import app.simple.inure.decorations.views.WebViewXMLViewer
import app.simple.inure.extensions.fragments.ScopedBottomSheetFragment

class HtmlViewer : ScopedBottomSheetFragment() {

private lateinit var webView: XMLWebView
private lateinit var webView: WebViewXMLViewer

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_html, container, false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsAnimation
import android.view.animation.PathInterpolator
import android.widget.ImageView
import androidx.activity.BackEventCompat
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.annotation.IntegerRes
import androidx.annotation.RequiresApi
Expand All @@ -28,7 +32,11 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.ArcMotion
import androidx.transition.ChangeBounds
import androidx.transition.Fade
import androidx.transition.TransitionManager
import androidx.transition.TransitionSeekController
import androidx.transition.TransitionSet
import app.simple.inure.R
import app.simple.inure.apk.utils.PackageUtils
import app.simple.inure.constants.BundleConstants
Expand All @@ -40,6 +48,10 @@ import app.simple.inure.dialogs.miscellaneous.Error.Companion.showError
import app.simple.inure.dialogs.miscellaneous.Loader
import app.simple.inure.dialogs.miscellaneous.Warning.Companion.showWarning
import app.simple.inure.interfaces.fragments.SureCallbacks
import app.simple.inure.math.Extensions.half
import app.simple.inure.math.Extensions.negate
import app.simple.inure.math.Extensions.zero
import app.simple.inure.math.Range.mapRange
import app.simple.inure.popups.behavior.PopupArcType
import app.simple.inure.popups.behavior.PopupTransitionType
import app.simple.inure.preferences.BehaviourPreferences
Expand Down Expand Up @@ -75,6 +87,12 @@ abstract class ScopedFragment : Fragment(), SharedPreferences.OnSharedPreference
protected var minimumHorizontalAngle = 80
protected var minimumVerticalAngle = 15

val transitionSet = TransitionSet().apply {
addTransition(Fade(Fade.MODE_OUT))
addTransition(ChangeBounds())
addTransition(Fade(Fade.MODE_IN))
}

/**
* [ScopedFragment]'s own [Handler] instance
*/
Expand Down Expand Up @@ -117,6 +135,10 @@ abstract class ScopedFragment : Fragment(), SharedPreferences.OnSharedPreference
}

animateBlur()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setupBackPressedCallback(view as ViewGroup)
}
}

private fun animateBlur() {
Expand Down Expand Up @@ -513,6 +535,86 @@ abstract class ScopedFragment : Fragment(), SharedPreferences.OnSharedPreference
}
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
open fun setupBackPressedCallback(view: ViewGroup) {
val windowWidth = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
requireActivity().windowManager.currentWindowMetrics.bounds.width()
} else {
zero()
}

val windowHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
requireActivity().windowManager.currentWindowMetrics.bounds.height()
} else {
zero()
}

val maxXShift = windowWidth / MAX_WINDOW_WIDTH

val callback = object : OnBackPressedCallback(enabled = true) {
var controller: TransitionSeekController? = null

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun handleOnBackStarted(backEvent: BackEventCompat) {
controller = TransitionManager.controlDelayedTransition(
view,
transitionSet
)
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
if (controller?.isReady == true) {
controller?.currentFraction = backEvent.progress
}

// Shift the view based on the swipe progress
val backProgress = backEvent.progress
val interpolatedProgress = EMPHASIZED_DECELERATE.getInterpolation(backProgress)
val totalTranslationY = backEvent.touchY
.mapRange(zero(), windowHeight, windowHeight.half().negate(), windowHeight.half())
.div(MAX_WINDOW_HEIGHT)

when (backEvent.swipeEdge) {
BackEventCompat.EDGE_LEFT -> {
view.translationX = interpolatedProgress * maxXShift
view.translationY = totalTranslationY * interpolatedProgress
}
BackEventCompat.EDGE_RIGHT -> {
view.translationX = -(interpolatedProgress * maxXShift)
view.translationY = totalTranslationY * interpolatedProgress
}
}

view.scaleX = 1F - (0.1F * interpolatedProgress)
view.scaleY = 1F - (0.1F * interpolatedProgress)
// view.alpha = 1F - (0.5F * interpolatedProgress)
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun handleOnBackPressed() {
Log.d(TAG, "handleOnBackPressed: ")
// Finish playing the transition when the user commits back )
this.isEnabled = false
popBackStack()
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun handleOnBackCancelled() {
Log.d(TAG, "handleOnBackCancelled: ")
// If the user cancels the back gesture, reset the state
resetCallbackState()
}

private fun resetCallbackState() {
// Animate the view back to its original position
view.animate().translationX(0F).scaleX(1F).scaleY(1F).start()
}
}

requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}

/**
* Return the {@link Application} this fragment is currently associated with.
*/
Expand Down Expand Up @@ -608,12 +710,6 @@ abstract class ScopedFragment : Fragment(), SharedPreferences.OnSharedPreference
protected fun openFragmentArc(fragment: ScopedFragment, icon: View, tag: String? = null, duration: Long? = null) {
fragment.setArcTransitions(duration ?: resources.getInteger(R.integer.animation_duration).toLong())

// try {
// (fragment.exitTransition as TransitionSet?)?.excludeTarget(icon, true)
// } catch (e: java.lang.ClassCastException) {
// (fragment.exitTransition as MaterialContainerTransform?)?.excludeTarget(icon, true)
// }

try {
val transaction = requireActivity().supportFragmentManager.beginTransaction().apply {
setReorderingAllowed(true)
Expand Down Expand Up @@ -739,6 +835,14 @@ abstract class ScopedFragment : Fragment(), SharedPreferences.OnSharedPreference
}

companion object {
private val EMPHASIZED_DECELERATE = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)

/**
* Lower values will result in a more emphasized movement
*/
private const val MAX_WINDOW_WIDTH = 20
private const val MAX_WINDOW_HEIGHT = 5

private const val TAG = "ScopedFragment"
}
}
20 changes: 19 additions & 1 deletion app/src/main/java/app/simple/inure/math/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,22 @@ object Extensions {
fun Double.percentOf(total: Double): Double = (this * 100.0 / total)

fun Float.getPercentPart(total: Float): Float = (this * total / 100.0).toFloat()
}

fun Float.negate(): Float = -this

fun Double.negate(): Double = -this

fun Int.negate(): Int = -this

fun Long.negate(): Long = -this

fun Int.half(): Int = this / 2

fun Long.half(): Long = this / 2

fun Float.half(): Float = this / 2.0f

fun Double.half(): Double = this / 2.0

fun zero() = 0
}
27 changes: 27 additions & 0 deletions app/src/main/java/app/simple/inure/math/Range.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package app.simple.inure.math

object Range {
fun Float.mapRange(fromMin: Float, fromMax: Float, toMin: Float, toMax: Float): Float {
return (this - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin
}

fun Float.mapRange(fromMin: Int, fromMax: Int, toMin: Int, toMax: Int): Float {
return (this - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin
}

fun Double.mapRange(fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double {
return (this - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin
}

fun Int.mapRange(fromMin: Int, fromMax: Int, toMin: Int, toMax: Int): Int {
return ((this - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin).toInt()
}

fun Long.mapRange(fromMin: Long, fromMax: Long, toMin: Long, toMax: Long): Long {
return ((this - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin).toLong()
}

fun Float.normalize(min: Float, max: Float): Float {
return (this - min) / (max - min)
}
}
4 changes: 4 additions & 0 deletions app/src/main/java/app/simple/inure/ui/panels/Home.kt
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@ class Home : ScopedFragment() {
/* no-op */
}

override fun setupBackPressedCallback(view: ViewGroup) {
/* no-op */
}

companion object {
fun newInstance(): Home {
val args = Bundle()
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/app/simple/inure/ui/viewers/XMLWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ import app.simple.inure.constants.BundleConstants
import app.simple.inure.constants.MimeConstants
import app.simple.inure.decorations.ripple.DynamicRippleImageButton
import app.simple.inure.decorations.typeface.TypeFaceTextView
import app.simple.inure.decorations.views.WebViewXMLViewer
import app.simple.inure.extensions.fragments.ScopedFragment
import app.simple.inure.factories.panels.XMLViewerViewModelFactory
import app.simple.inure.popups.viewers.PopupXmlViewer
import app.simple.inure.util.NullSafety.isNull
import app.simple.inure.util.ViewUtils.gone
import app.simple.inure.viewmodels.viewers.XMLViewerViewModel
import java.io.IOException
import app.simple.inure.decorations.views.XMLWebView as ViewsXMLWebView

class XMLWebView : ScopedFragment() {

private lateinit var manifest: ViewsXMLWebView
private lateinit var manifest: WebViewXMLViewer
private lateinit var name: TypeFaceTextView
private lateinit var options: DynamicRippleImageButton
private lateinit var progress: ProgressBar
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/layout/dialog_html.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
android:fadingEdgeLength="25dp"
android:requiresFadingEdge="vertical">

<app.simple.inure.decorations.views.XMLWebView
<app.simple.inure.decorations.views.WebViewXMLViewer
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/layout/fragment_web_viewer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="1px" />

<app.simple.inure.decorations.views.XMLWebView
<app.simple.inure.decorations.views.WebViewXMLViewer
android:id="@+id/source_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Expand Down

0 comments on commit 8f01dbf

Please sign in to comment.