Redo controls view

Redo the controls view to support a slide enter/exit animation, along with fixes to elevation problems on certain devices.
This commit is contained in:
OxygenCobalt 2021-02-13 14:49:57 -07:00
parent 2b24d6661e
commit a4801bdf2f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 164 additions and 65 deletions

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio package org.oxycblt.auxio
import android.animation.LayoutTransition
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -9,6 +10,7 @@ import android.view.ViewGroup
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
@ -69,6 +71,13 @@ class MainFragment : Fragment() {
binding.lifecycleOwner = this binding.lifecycleOwner = this
// Apply custom interpolation effects
// The container layout types differ in the land/port layouts so it has to be cast to their parent instead.
(binding.controlsContainer as ViewGroup).layoutTransition = LayoutTransition().apply {
setInterpolator(LayoutTransition.APPEARING, FastOutSlowInInterpolator())
setInterpolator(LayoutTransition.DISAPPEARING, FastOutSlowInInterpolator())
}
binding.navBar.apply { binding.navBar.apply {
itemIconTintList = navTints itemIconTintList = navTints
itemTextColor = navTints itemTextColor = navTints
@ -150,7 +159,9 @@ class MainFragment : Fragment() {
if (song == null) { if (song == null) {
logD("Hiding CompactPlaybackFragment since no song is being played.") logD("Hiding CompactPlaybackFragment since no song is being played.")
if (!isLandscape(resources)) { if (isLandscape(resources)) {
binding.compactPlayback.visibility = View.INVISIBLE
} else {
binding.compactPlayback.visibility = View.GONE binding.compactPlayback.visibility = View.GONE
} }

View file

@ -57,10 +57,6 @@ class CompactPlaybackFragment : Fragment() {
} }
} }
if (playbackModel.song.value == null) {
setInvisible(true)
}
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
playbackModel.song.observe(viewLifecycleOwner) { playbackModel.song.observe(viewLifecycleOwner) {
@ -69,9 +65,6 @@ class CompactPlaybackFragment : Fragment() {
binding.song = it binding.song = it
binding.playbackProgress.max = it.seconds.toInt() binding.playbackProgress.max = it.seconds.toInt()
setInvisible(false)
} else {
setInvisible(true)
} }
} }
@ -117,20 +110,4 @@ class CompactPlaybackFragment : Fragment() {
} }
} }
} }
/**
* Set this fragment to be invisible, if needed. Only runs in landscape mode.
*/
private fun setInvisible(invisible: Boolean) {
// Does not run in landscape
if (!isLandscape(resources)) return
val visibility = if (invisible) View.INVISIBLE else View.VISIBLE
binding.playbackLayout.children.forEach {
it.visibility = visibility
}
binding.root.isEnabled = !invisible
}
} }

View file

@ -0,0 +1,98 @@
package org.oxycblt.auxio.ui
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.logE
import java.lang.reflect.Field
/**
* Hack layout that fixes an issue where disappearing views would draw over non-disappearing
* views when animated with a stock LayoutTransition. Adapted from this StackOverflow answer:
* https://stackoverflow.com/a/35087229
*/
class SlideLinearLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = -1
) : LinearLayout(context, attrs, defStyleAttr) {
private val disappearingChildrenField: Field? = try {
ViewGroup::class.java.getDeclaredField("mDisappearingChildren").also {
it.isAccessible = true
}
} catch (e: NoSuchFieldException) {
logE("Could not get mDisappearingChildren.")
null
}
private var disappearingChildren: List<View>? = null
private var dumpView: View? = null
private var doDrawingTrick: Boolean = false
init {
if (disappearingChildrenField != null) {
// Create a junk view and add it, which makes all the magic happen [I think??].
dumpView = View(context)
addView(dumpView, 0, 0)
}
}
override fun dispatchDraw(canvas: Canvas?) {
doDrawingTrick = beforeDispatchDraw()
super.dispatchDraw(canvas)
if (doDrawingTrick) {
doDrawingTrick = false
}
}
override fun drawChild(canvas: Canvas?, child: View?, drawingTime: Long): Boolean {
val children = getDisappearingChildren()
// I have no idea what this code does.
if (doDrawingTrick && children != null) {
if (child == dumpView) {
var more = false
children.forEach {
more = more or super.drawChild(canvas, it, drawingTime)
}
return more
} else if (children.contains(child)) {
return false
}
}
return super.drawChild(canvas, child, drawingTime)
}
private fun beforeDispatchDraw(): Boolean {
val children = getDisappearingChildren()
if (children == null || children.isEmpty() || childCount <= 1) { // Junk view included
return false
}
return true
}
@Suppress("UNCHECKED_CAST")
private fun getDisappearingChildren(): List<View>? {
if (disappearingChildrenField == null || disappearingChildren != null) {
return disappearingChildren
}
// If there is no list of disappearing children yet, attempt to get it.
try {
disappearingChildren = disappearingChildrenField.get(this) as List<View>?
} catch (e: IllegalAccessException) {
logD("Could not get list of disappearing children.")
}
return disappearingChildren
}
}

View file

@ -17,34 +17,41 @@
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/compact_playback" app:layout_constraintBottom_toTopOf="@+id/controls_container"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_explore" app:navGraph="@navigation/nav_explore"
tools:layout="@layout/fragment_library" /> tools:layout="@layout/fragment_library" />
<androidx.fragment.app.FragmentContainerView <LinearLayout
android:id="@+id/compact_playback" android:id="@+id/controls_container"
android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_high" android:baselineAligned="false"
android:outlineProvider="bounds" android:animateLayoutChanges="true"
app:layout_constraintBottom_toBottomOf="parent" android:elevation="@dimen/elevation_normal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintStart_toEndOf="@+id/nav_bar"
tools:layout="@layout/fragment_compact_playback" />
<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_bar" android:id="@+id/nav_bar"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="match_parent"
android:background="@color/background" android:background="@color/background"
app:labelVisibilityMode="selected" android:elevation="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:elevation="0dp"
app:layout_constraintStart_toStartOf="parent" android:layout_weight="1.5"
app:layout_constraintTop_toTopOf="@+id/compact_playback" app:menu="@menu/menu_nav" />
app:menu="@menu/menu_nav" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/compact_playback"
android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
tools:layout="@layout/fragment_compact_playback" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -17,33 +17,40 @@
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/compact_playback" app:layout_constraintBottom_toTopOf="@+id/controls_container"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_explore" app:navGraph="@navigation/nav_explore"
tools:layout="@layout/fragment_library" /> tools:layout="@layout/fragment_library" />
<androidx.fragment.app.FragmentContainerView <org.oxycblt.auxio.ui.SlideLinearLayout
android:id="@+id/compact_playback" android:id="@+id/controls_container"
android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_high" android:animateLayoutChanges="true"
android:outlineProvider="bounds" android:elevation="@dimen/elevation_normal"
app:layout_constraintBottom_toTopOf="@+id/nav_bar" app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:layout="@layout/fragment_compact_playback" />
<com.google.android.material.bottomnavigation.BottomNavigationView <androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_bar" android:id="@+id/compact_playback"
android:layout_width="match_parent" android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:background="@color/background" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/nav_bar"
app:layout_constraintEnd_toEndOf="parent" tools:layout="@layout/fragment_compact_playback" />
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/menu_nav" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/background"
android:elevation="0dp"
app:elevation="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/menu_nav" />
</org.oxycblt.auxio.ui.SlideLinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -49,7 +49,6 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_recycler" android:id="@+id/search_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"