Skip to content

Commit 37a9459

Browse files
authored
[UI] Enable colored app bars on scroll (#211)
* Extract fragment scroll listener to separate file * Replace canRefresh with isScrolled * Add empty helper class to animate app bar color * Implement AppBarColorAnimator * Rename getRefreshScrollingView() to getScrollingView() * Set isScrolled on drag start * Clear isScrolled on fling to top * Add getSyncParams() to fragments * Convert getAppBars() to property * Tint TabLayout background drawable
1 parent 58d9dec commit 37a9459

24 files changed

+254
-100
lines changed

Diff for: app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import kotlinx.coroutines.withContext
1919
import pl.szczodrzynski.edziennik.MainActivity
2020
import pl.szczodrzynski.edziennik.R
2121
import pl.szczodrzynski.edziennik.data.db.entity.Profile
22+
import pl.szczodrzynski.edziennik.data.enums.FeatureType
2223
import pl.szczodrzynski.edziennik.data.enums.MetadataType
2324
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding
2425
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding
@@ -45,6 +46,7 @@ class AgendaFragment : BaseFragment<ViewBinding, MainActivity>(
4546

4647
override fun getFab() = R.string.add to CommunityMaterial.Icon3.cmd_plus
4748
override fun getMarkAsReadType() = MetadataType.EVENT
49+
override fun getSyncParams() = FeatureType.AGENDA to null
4850
override fun getBottomSheetItems() = listOf(
4951
BottomSheetPrimaryItem(true)
5052
.withTitle(R.string.menu_add_event)
@@ -114,6 +116,7 @@ class AgendaFragment : BaseFragment<ViewBinding, MainActivity>(
114116
private suspend fun createDefaultAgendaView(b: FragmentAgendaDefaultBinding) {
115117
if (!isAdded)
116118
return
119+
canRefreshDisabled = true
117120
checkEventTypes()
118121
delay(500)
119122

Diff for: app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceFragment.kt

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.ui.attendance
77
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
88
import pl.szczodrzynski.edziennik.MainActivity
99
import pl.szczodrzynski.edziennik.R
10+
import pl.szczodrzynski.edziennik.data.enums.FeatureType
1011
import pl.szczodrzynski.edziennik.data.enums.MetadataType
1112
import pl.szczodrzynski.edziennik.databinding.BasePagerFragmentBinding
1213
import pl.szczodrzynski.edziennik.ext.Bundle
@@ -32,6 +33,7 @@ class AttendanceFragment : PagerFragment<BasePagerFragmentBinding, MainActivity>
3233
}
3334

3435
override fun getMarkAsReadType() = MetadataType.ATTENDANCE
36+
override fun getSyncParams() = FeatureType.ATTENDANCE to null
3537
override fun getBottomSheetItems() = listOf(
3638
BottomSheetPrimaryItem(true)
3739
.withTitle(R.string.menu_attendance_config)

Diff for: app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class AttendanceListFragment : BaseFragment<AttendanceListFragmentBinding, MainA
2929
inflater = AttendanceListFragmentBinding::inflate,
3030
) {
3131

32-
override fun getRefreshScrollingView() = b.list
32+
override fun getScrollingView() = b.list
3333

3434
private var viewType = AttendanceFragment.VIEW_DAYS
3535
private var expandSubjectId = 0L

Diff for: app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class AttendanceSummaryFragment : BaseFragment<AttendanceSummaryFragmentBinding,
4343
private var periodSelection = 0
4444
}
4545

46-
override fun getRefreshScrollingView() = b.scrollView
46+
override fun getScrollingView() = b.scrollView
4747

4848
private val manager
4949
get() = app.attendanceManager

Diff for: app/src/main/java/pl/szczodrzynski/edziennik/ui/base/fragment/ActivityUtil.kt

+2-47
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@
44

55
package pl.szczodrzynski.edziennik.ui.base.fragment
66

7-
import android.annotation.SuppressLint
8-
import android.view.MotionEvent
9-
import android.view.View
107
import android.widget.Toast
11-
import androidx.recyclerview.widget.RecyclerView
128
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
139
import kotlinx.coroutines.Dispatchers
1410
import kotlinx.coroutines.launch
@@ -18,49 +14,6 @@ import pl.szczodrzynski.edziennik.ui.login.LoginActivity
1814
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
1915
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
2016

21-
@SuppressLint("ClickableViewAccessibility")
22-
internal fun BaseFragment<*, *>.setupCanRefresh() {
23-
when (val view = getRefreshScrollingView()) {
24-
is RecyclerView -> {
25-
canRefresh = !view.canScrollVertically(-1)
26-
view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
27-
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
28-
// disable refresh when scrolled down
29-
if (recyclerView.canScrollVertically(-1))
30-
canRefresh = false
31-
// enable refresh when scrolled to the top and not scrolling anymore
32-
else if (newState == RecyclerView.SCROLL_STATE_IDLE)
33-
canRefresh = true
34-
}
35-
})
36-
}
37-
38-
is View -> {
39-
canRefresh = !view.canScrollVertically(-1)
40-
var isTouched = false
41-
view.setOnTouchListener { _, event ->
42-
// keep track of the touch state
43-
when (event.action) {
44-
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> isTouched = false
45-
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> isTouched = true
46-
}
47-
// disable refresh when scrolled down
48-
if (view.canScrollVertically(-1))
49-
canRefresh = false
50-
// enable refresh when scrolled to the top and not touching anymore
51-
else if (!isTouched)
52-
canRefresh = true
53-
false
54-
}
55-
}
56-
57-
else -> {
58-
// dispatch the default value to the activity
59-
canRefresh = canRefresh
60-
}
61-
}
62-
}
63-
6417
internal fun BaseFragment<*, *>.setupMainActivity(activity: MainActivity) {
6518
val items = getBottomSheetItems().toMutableList()
6619
getMarkAsReadType()?.let { metadataType ->
@@ -97,6 +50,8 @@ internal fun BaseFragment<*, *>.setupMainActivity(activity: MainActivity) {
9750
}
9851
}
9952
}
53+
54+
appBars += activity.navView.toolbar
10055
}
10156

10257
internal fun BaseFragment<*, *>.setupLoginActivity(activity: LoginActivity) {}

Diff for: app/src/main/java/pl/szczodrzynski/edziennik/ui/base/fragment/BaseFragment.kt

+49-15
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import android.view.ViewGroup
1212
import androidx.appcompat.app.AppCompatActivity
1313
import androidx.fragment.app.Fragment
1414
import androidx.viewbinding.ViewBinding
15+
import com.google.gson.JsonObject
1516
import com.mikepenz.iconics.typeface.IIcon
1617
import kotlinx.coroutines.CoroutineScope
1718
import kotlinx.coroutines.Dispatchers
1819
import kotlinx.coroutines.Job
1920
import org.greenrobot.eventbus.EventBus
2021
import pl.szczodrzynski.edziennik.App
2122
import pl.szczodrzynski.edziennik.MainActivity
23+
import pl.szczodrzynski.edziennik.data.enums.FeatureType
2224
import pl.szczodrzynski.edziennik.data.enums.MetadataType
2325
import pl.szczodrzynski.edziennik.ext.registerSafe
2426
import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
@@ -38,31 +40,37 @@ abstract class BaseFragment<B : ViewBinding, A : AppCompatActivity>(
3840

3941
private var isViewReady: Boolean = false
4042
private var inState: Bundle? = null
43+
private var appBarAnimator: AppBarColorAnimator? = null
4144

4245
/**
43-
* Enables or disables the activity's SwipeRefreshLayout.
44-
* Use only if [getRefreshScrollingView] is not used.
45-
*
46-
* The [PagerFragment] manages its [canRefresh] state
47-
* based on the value of the currently selected page.
46+
* Whether the view is currently being scrolled
47+
* or is left scrolled away from the top.
4848
*/
49-
internal var canRefresh = false
50-
set(value) {
49+
internal var isScrolled = false
50+
set(value) { // cannot be private - PagerFragment onPageScrollStateChanged
5151
field = value
52-
(activity as? MainActivity)?.swipeRefreshLayout?.isEnabled =
53-
!canRefreshDisabled && value
52+
dispatchCanRefresh()
53+
appBarAnimator?.dispatchLiftOnScroll()
5454
}
5555

5656
/**
57-
* Forcefully disables the activity's SwipeRefreshLayout
58-
* if [getRefreshScrollingView] is used.
57+
* Forcefully disables the activity's SwipeRefreshLayout.
58+
*
59+
* The [PagerFragment] manages its [canRefreshDisabled] state
60+
* based on the value of the currently selected page.
5961
*/
6062
internal var canRefreshDisabled = false
6163
set(value) {
6264
field = value
63-
canRefresh = canRefresh
65+
dispatchCanRefresh()
6466
}
6567

68+
/**
69+
* A list of views (usually app bars) that should have their
70+
* background color elevated when the fragment is scrolled.
71+
*/
72+
internal var appBars = mutableSetOf<View>()
73+
6674
private var job = Job()
6775
final override val coroutineContext: CoroutineContext
6876
get() = job + Dispatchers.Main
@@ -83,6 +91,7 @@ abstract class BaseFragment<B : ViewBinding, A : AppCompatActivity>(
8391
?: return null
8492
isViewReady = false // reinitialize the view in onResume()
8593
inState = savedInstanceState // save the instance state for onResume()
94+
appBarAnimator = AppBarColorAnimator(activity, appBars)
8695
return b.root
8796
}
8897

@@ -92,9 +101,17 @@ abstract class BaseFragment<B : ViewBinding, A : AppCompatActivity>(
92101
if (!isAdded || isViewReady)
93102
return
94103
isViewReady = true
95-
setupCanRefresh()
104+
// setup the activity (bottom sheet, FAB, etc.)
105+
// run before setupScrollListener {} to populate appBars
96106
(activity as? MainActivity)?.let(::setupMainActivity)
97107
(activity as? LoginActivity)?.let(::setupLoginActivity)
108+
// listen to scroll state changes
109+
var first = true
110+
setupScrollListener {
111+
if (isScrolled != it || first)
112+
isScrolled = it
113+
first = false
114+
}
98115
// let the UI transition for a moment
99116
startCoroutineTimer(100L) {
100117
if (!isAdded)
@@ -141,9 +158,10 @@ abstract class BaseFragment<B : ViewBinding, A : AppCompatActivity>(
141158

142159
/**
143160
* Called to retrieve the scrolling view contained in the fragment.
144-
* The scrolling view is configured to act nicely with the SwipeRefreshLayout.
161+
* The scrolling view is configured to work nicely with the app bars
162+
* and the SwipeRefreshLayout.
145163
*/
146-
open fun getRefreshScrollingView(): View? = null
164+
open fun getScrollingView(): View? = null
147165

148166
/**
149167
* Called to retrieve the FAB label resource and the icon.
@@ -157,6 +175,22 @@ abstract class BaseFragment<B : ViewBinding, A : AppCompatActivity>(
157175
*/
158176
open fun getMarkAsReadType(): MetadataType? = null
159177

178+
/**
179+
* Called to retrieve the [FeatureType] this fragment is associated with.
180+
* May also return arguments for the sync task.
181+
*
182+
* If not provided, swipe-to-refresh is disabled and the manual sync dialog
183+
* selects all features by default.
184+
*
185+
* If [FeatureType] is null, all features are synced (and selected by the
186+
* manual sync dialog).
187+
*
188+
* It is important to return the desired [FeatureType] from the first
189+
* call of this method, which runs before [onViewReady]. Otherwise,
190+
* swipe-to-refresh will not be enabled unless the view is scrolled.
191+
*/
192+
open fun getSyncParams(): Pair<FeatureType?, JsonObject?>? = null
193+
160194
/**
161195
* Called to retrieve any extra bottom sheet items that should be displayed.
162196
*/

Diff for: app/src/main/java/pl/szczodrzynski/edziennik/ui/base/fragment/PagerFragment.kt

+24-11
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ abstract class PagerFragment<B : ViewBinding, A : AppCompatActivity>(
2323
inflater: ((inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> B)?,
2424
) : BaseFragment<B, A>(inflater) {
2525

26-
private lateinit var pages: List<Pair<Fragment, String>>
27-
private val fragmentCache = mutableMapOf<Int, Fragment>()
26+
private lateinit var pages: List<Pair<BaseFragment<*, *>, String>>
27+
private val fragmentCache = mutableMapOf<Int, BaseFragment<*, *>>()
2828

2929
/**
3030
* Stores the default page index that is activated when
@@ -37,6 +37,18 @@ abstract class PagerFragment<B : ViewBinding, A : AppCompatActivity>(
3737
*/
3838
protected open var savedPageSelection = -1
3939

40+
protected val currentFragment: BaseFragment<*, *>?
41+
get() = fragmentCache[getViewPager().currentItem]
42+
43+
final override fun getScrollingView() = null
44+
override fun getSyncParams() = currentFragment?.getSyncParams()
45+
46+
override fun onResume() {
47+
// add TabLayout before super's setupScrollListener {}
48+
appBars += getTabLayout()
49+
super.onResume()
50+
}
51+
4052
override suspend fun onViewReady(savedInstanceState: Bundle?) {
4153
if (savedPageSelection == -1)
4254
savedPageSelection = savedInstanceState?.getInt("pageSelection") ?: 0
@@ -47,6 +59,7 @@ abstract class PagerFragment<B : ViewBinding, A : AppCompatActivity>(
4759
override fun getItemCount() = getPageCount()
4860
override fun createFragment(position: Int): Fragment {
4961
val fragment = getPageFragment(position)
62+
fragment.appBars += getTabLayout()
5063
fragmentCache[position] = fragment
5164
return fragment
5265
}
@@ -58,15 +71,15 @@ abstract class PagerFragment<B : ViewBinding, A : AppCompatActivity>(
5871
it.setCurrentItem(savedPageSelection, false)
5972
it.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
6073
override fun onPageScrollStateChanged(state: Int) {
61-
canRefresh = when (state) {
62-
ViewPager2.SCROLL_STATE_IDLE -> {
63-
val fragment =
64-
fragmentCache[it.currentItem] as? BaseFragment<*, *>
65-
fragment != null && !fragment.canRefreshDisabled && fragment.canRefresh
66-
}
67-
68-
else -> false
74+
if (state != ViewPager2.SCROLL_STATE_IDLE) {
75+
// disable swipe-to-refresh during scrolling
76+
canRefreshDisabled = true
77+
return
6978
}
79+
// take child fragment's values
80+
val fragment = currentFragment
81+
canRefreshDisabled = fragment?.canRefreshDisabled == true
82+
isScrolled = fragment?.isScrolled == true
7083
}
7184

7285
override fun onPageSelected(position: Int) {
@@ -125,7 +138,7 @@ abstract class PagerFragment<B : ViewBinding, A : AppCompatActivity>(
125138
* Only used with the default implementation of [getPageCount], [getPageFragment]
126139
* and [getPageTitle].
127140
*/
128-
open suspend fun onCreatePages() = listOf<Pair<Fragment, String>>()
141+
open suspend fun onCreatePages() = listOf<Pair<BaseFragment<*, *>, String>>()
129142

130143
open fun getPageCount() = pages.size
131144
open fun getPageFragment(position: Int) = pages[position].first

0 commit comments

Comments
 (0)