forked from github/szkolny
[UI/Timetable] Add current time marker line. (#30)
This commit is contained in:
parent
e25ca930e0
commit
8609956ae7
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
app/src/main/res/drawable/timetable_marker_triangle.xml
Normal file
15
app/src/main/res/drawable/timetable_marker_triangle.xml
Normal 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>
|
||||||
|
|
@ -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>
|
|
53
app/src/main/res/layout/timetable_day_fragment.xml
Normal file
53
app/src/main/res/layout/timetable_day_fragment.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user