[UI/Timetable] Add current time marker line. (#30)

This commit is contained in:
Kuba Szczodrzyński 2021-04-14 22:41:06 +02:00 committed by GitHub
parent e25ca930e0
commit 8609956ae7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 157 additions and 86 deletions

View File

@ -5,16 +5,16 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable package pl.szczodrzynski.edziennik.ui.modules.timetable
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.asynclayoutinflater.view.AsyncLayoutInflater import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.lifecycle.Observer import androidx.core.view.updateLayoutParams
import com.linkedin.android.tachyon.DayView import com.linkedin.android.tachyon.DayView
import com.linkedin.android.tachyon.DayViewConfig import com.linkedin.android.tachyon.DayViewConfig
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -24,14 +24,15 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR
import pl.szczodrzynski.edziennik.utils.ListenerScrollView
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.min import kotlin.math.min
@ -44,34 +45,28 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
private lateinit var app: App private lateinit var app: App
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var inflater: AsyncLayoutInflater private lateinit var inflater: AsyncLayoutInflater
private lateinit var b: TimetableDayFragmentBinding
private val job: Job = Job() private val job: Job = Job()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main get() = job + Dispatchers.Main
private var timeIndicatorJob: Job? = null
private lateinit var date: Date private lateinit var date: Date
private var startHour = DEFAULT_START_HOUR private var startHour = DEFAULT_START_HOUR
private var endHour = DEFAULT_END_HOUR private var endHour = DEFAULT_END_HOUR
private var firstEventMinute = 24 * 60 private var firstEventMinute = 24 * 60
private var paddingTop = 0
private val manager private val manager
get() = app.timetableManager get() = app.timetableManager
// find SwipeRefreshLayout in the hierarchy // find SwipeRefreshLayout in the hierarchy
private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) }
// the day ScrollView
private val dayScrollDelegate = lazy {
val dayScroll = ListenerScrollView(context!!)
dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
dayScroll.setOnRefreshLayoutEnabledListener { enabled ->
refreshLayout?.isEnabled = enabled
}
dayScroll
}
private val dayScroll by dayScrollDelegate
// the lesson DayView
private val dayView by lazy { private val dayView by lazy {
val dayView = DayView(context!!, DayViewConfig( val dayView = DayView(activity, DayViewConfig(
startHour = startHour, startHour = startHour,
endHour = endHour, endHour = endHour,
dividerHeight = 1.dp, dividerHeight = 1.dp,
@ -83,37 +78,33 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
eventMargin = 2.dp eventMargin = 2.dp
), true) ), true)
dayView.setPadding(10.dp) dayView.setPadding(10.dp)
dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) return@lazy dayView
dayScroll.addView(dayView)
dayView
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
this.inflater = AsyncLayoutInflater(context!!) this.inflater = AsyncLayoutInflater(requireContext())
date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday() date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday()
startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR
endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR
return FrameLayout(activity).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) b = TimetableDayFragmentBinding.inflate(inflater, null, false)
addView(ProgressBar(activity).apply { return b.root
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)
})
}
} }
override fun onPageCreated(): Boolean { override fun onPageCreated(): Boolean {
// observe lesson database // observe lesson database
app.db.timetableDao().getAllForDate(App.profileId, date).observe(this, Observer { lessons -> app.db.timetableDao().getAllForDate(App.profileId, date).observe(this) { lessons ->
launch { launch {
val events = withContext(Dispatchers.Default) { val events = withContext(Dispatchers.Default) {
app.db.eventDao().getAllByDateNow(App.profileId, date) app.db.eventDao().getAllByDateNow(App.profileId, date)
} }
processLessonList(lessons, events) processLessonList(lessons, events)
} }
}) }
return true return true
} }
@ -121,9 +112,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
private fun processLessonList(lessons: List<LessonFull>, events: List<EventFull>) { private fun processLessonList(lessons: List<LessonFull>, events: List<EventFull>) {
// no lessons - timetable not downloaded yet // no lessons - timetable not downloaded yet
if (lessons.isEmpty()) { if (lessons.isEmpty()) {
inflater.inflate(R.layout.timetable_no_timetable, view as FrameLayout?) { view, _, parent -> inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ ->
parent?.removeAllViews() b.root.removeAllViews()
parent?.addView(view) b.root.addView(view)
val b = TimetableNoTimetableBinding.bind(view) val b = TimetableNoTimetableBinding.bind(view)
val weekStart = date.weekStart.stringY_m_d val weekStart = date.weekStart.stringY_m_d
b.noTimetableSync.onClick { b.noTimetableSync.onClick {
@ -144,9 +136,9 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
} }
// one lesson indicating a day without lessons // one lesson indicating a day without lessons
if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) {
inflater.inflate(R.layout.timetable_no_lessons, view as FrameLayout?) { view, _, parent -> inflater.inflate(R.layout.timetable_no_lessons, b.root) { view, _, _ ->
parent?.removeAllViews() b.root.removeAllViews()
parent?.addView(view) b.root.addView(view)
} }
return return
} }
@ -158,12 +150,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
return return
} }
// clear the root view and add the ScrollView b.scrollView.isVisible = true
(view as FrameLayout?)?.removeAllViews() b.dayFrame.removeView(b.dayView)
(view as FrameLayout?)?.addView(dayScroll) b.dayFrame.addView(dayView, 0)
// Inflate a label view for each hour the day view will display // Inflate a label view for each hour the day view will display
val hourLabelViews = ArrayList<View>() val hourLabelViews = mutableListOf<View>()
for (i in dayView.startHour..dayView.endHour) { for (i in dayView.startHour..dayView.endHour) {
if (!isAdded) if (!isAdded)
continue continue
@ -172,6 +164,11 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
hourLabelViews.add(hourLabelView) hourLabelViews.add(hourLabelView)
} }
dayView.setHourLabelViews(hourLabelViews) dayView.setHourLabelViews(hourLabelViews)
// measure dayView top padding needed for the timeIndicator
hourLabelViews.getOrNull(0)?.let {
it.measure(0, 0)
paddingTop = it.measuredHeight / 2 + dayView.paddingTop
}
lessons.forEach { it.showAsUnseen = !it.seen } lessons.forEach { it.showAsUnseen = !it.seen }
@ -202,8 +199,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
// Try to recycle an existing event view if there are enough left, otherwise inflate // Try to recycle an existing event view if there are enough left, otherwise inflate
// a new one // a new one
val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, dayView, false)) val eventView =
?: continue (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(
R.layout.timetable_lesson,
dayView,
false
)) ?: continue
val lb = TimetableLessonBinding.bind(eventView) val lb = TimetableLessonBinding.bind(eventView)
eventViews += eventView eventViews += eventView
@ -291,16 +292,50 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute)) eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
} }
updateTimeIndicator()
dayView.setEventViews(eventViews, eventTimeRanges) dayView.setEventViews(eventViews, eventTimeRanges)
val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight
dayScroll.scrollTo(0, firstEventTop.toInt()) b.scrollView.scrollTo(0, firstEventTop.toInt())
b.progressBar.isVisible = false
}
private fun updateTimeIndicator() {
val time = Time.getNow()
val isTimeInView =
date == Date.getToday() && time.hour in dayView.startHour..dayView.endHour
b.timeIndicator.isVisible = isTimeInView
b.timeIndicatorMarker.isVisible = isTimeInView
if (isTimeInView) {
val startTime = Time(dayView.startHour, 0, 0)
val seconds = time.inSeconds - startTime.inSeconds * 1f
b.timeIndicator.updateLayoutParams<FrameLayout.LayoutParams> {
topMargin = (seconds * dayView.minuteHeight / 60f).toInt() + paddingTop
}
b.timeIndicatorMarker.updateLayoutParams<FrameLayout.LayoutParams> {
topMargin = b.timeIndicator.marginTop - (16.dp / 2) + (1.dp / 2)
}
}
if (timeIndicatorJob == null) {
timeIndicatorJob = startCoroutineTimer(repeatMillis = 30000) {
updateTimeIndicator()
}
}
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (dayScrollDelegate.isInitialized()) {
val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight
dayScroll.scrollTo(0, firstEventTop.toInt()) b.scrollView.scrollTo(0, firstEventTop.toInt())
} updateTimeIndicator()
}
override fun onPause() {
super.onPause()
timeIndicatorJob?.cancel()
timeIndicatorJob = null
} }
} }

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-4-14.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/md_red_500"
android:pathData="M0,4V20L12,12.25" />
</vector>

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:id="@+id/dayScroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.linkedin.android.tachyon.DayView
android:id="@+id/day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
app:dividerHeight="1dp"
app:endHour="18"
app:eventMargin="2dp"
app:halfHourDividerColor="#e0e0e0"
app:halfHourHeight="60dp"
app:hourDividerColor="#b0b0b0"
app:hourLabelMarginEnd="10dp"
app:hourLabelWidth="40dp"
app:startHour="5"
tools:visibility="gone"/>
</ScrollView>
</FrameLayout>
</layout>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-4-14.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<pl.szczodrzynski.edziennik.utils.ListenerScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<FrameLayout
android:id="@+id/dayFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_height="match_parent">
<View
android:id="@+id/timeIndicatorMarker"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginHorizontal="8dp"
android:background="@drawable/timetable_marker_triangle"
tools:layout_marginTop="92.5dp" />
<View
android:id="@+id/timeIndicator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="8dp"
android:background="@color/md_red_500"
tools:layout_marginTop="100dp" />
<com.linkedin.android.tachyon.DayView
android:id="@+id/dayView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_height="match_parent" />
</FrameLayout>
</pl.szczodrzynski.edziennik.utils.ListenerScrollView>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>