Allow direct access to weekend from day navigation when there is any lesson during weekend (#2326)

This commit is contained in:
Mikołaj Pich 2023-10-23 13:05:46 +02:00 committed by GitHub
parent 9d62410530
commit 83527d91f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 172 additions and 63 deletions

View File

@ -148,6 +148,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
binding.attendanceNavDate.text = date binding.attendanceNavDate.text = date
} }
override fun showNavigation(show: Boolean) {
binding.attendanceNavContainer.isVisible = show
}
override fun clearData() { override fun clearData() {
with(attendanceAdapter) { with(attendanceAdapter) {
items = emptyList() items = emptyList()
@ -281,7 +285,9 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) presenter.currentDate?.let {
outState.putLong(SAVED_DATE_KEY, it.toEpochDay())
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -3,10 +3,14 @@ package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint import android.annotation.SuppressLint
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
@ -14,6 +18,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDate.now import java.time.LocalDate.now
import java.time.LocalDate.ofEpochDay import java.time.LocalDate.ofEpochDay
@ -28,9 +33,10 @@ class AttendancePresenter @Inject constructor(
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<AttendanceView>(errorHandler, studentRepository) { ) : BasePresenter<AttendanceView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = now().previousOrSameSchoolDay private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
lateinit var currentDate: LocalDate var currentDate: LocalDate? = null
private set private set
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
@ -44,27 +50,34 @@ class AttendancePresenter @Inject constructor(
view.initView() view.initView()
Timber.i("Attendance view was initialized") Timber.i("Attendance view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError errorHandler.showErrorMessage = ::showErrorViewOnError
reloadView(ofEpochDay(date ?: baseDate.toEpochDay())) currentDate = date?.let(::ofEpochDay)
loadData() loadData()
if (currentDate.isHolidays) setBaseDateOnHolidays()
} }
fun onPreviousDay() { fun onPreviousDay() {
val date = if (isWeekendHasLessons) {
currentDate?.minusDays(1)
} else currentDate?.previousSchoolDay
view?.finishActionMode() view?.finishActionMode()
attendanceToExcuseList.clear() attendanceToExcuseList.clear()
reloadView(currentDate.previousSchoolDay) reloadView(date ?: return)
loadData() loadData()
} }
fun onNextDay() { fun onNextDay() {
val date = if (isWeekendHasLessons) {
currentDate?.plusDays(1)
} else currentDate?.nextSchoolDay
view?.finishActionMode() view?.finishActionMode()
attendanceToExcuseList.clear() attendanceToExcuseList.clear()
reloadView(currentDate.nextSchoolDay) reloadView(date ?: return)
loadData() loadData()
} }
fun onPickDate() { fun onPickDate() {
view?.showDatePickerDialog(currentDate) view?.showDatePickerDialog(currentDate ?: return)
} }
fun onDateSet(year: Int, month: Int, day: Int) { fun onDateSet(year: Int, month: Int, day: Int) {
@ -93,10 +106,8 @@ class AttendancePresenter @Inject constructor(
Timber.i("Attendance view is reselected") Timber.i("Attendance view is reselected")
view?.let { view -> view?.let { view ->
if (view.currentStackSize == 1) { if (view.currentStackSize == 1) {
baseDate = now().previousOrSameSchoolDay if (currentDate != initialDate) {
reloadView(initialDate ?: return)
if (currentDate != baseDate) {
reloadView(baseDate)
loadData() loadData()
} else if (!view.isViewEmpty) { } else if (!view.isViewEmpty) {
view.resetView() view.resetView()
@ -188,19 +199,6 @@ class AttendancePresenter @Inject constructor(
return true return true
} }
private fun setBaseDateOnHolidays() {
flow {
val student = studentRepository.getCurrentStudent()
emit(semesterRepository.getCurrentSemester(student))
}.catch {
Timber.i("Loading semester result: An exception occurred")
}.onEach {
baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear)
currentDate = baseDate
reloadNavigation()
}.launch("holidays")
}
private fun loadData(forceRefresh: Boolean = false) { private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading attendance data started") Timber.i("Loading attendance data started")
@ -211,11 +209,13 @@ class AttendancePresenter @Inject constructor(
isParent = student.isParent isParent = student.isParent
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester)
attendanceRepository.getAttendance( attendanceRepository.getAttendance(
student = student, student = student,
semester = semester, semester = semester,
start = currentDate, start = currentDate ?: now(),
end = currentDate, end = currentDate ?: now(),
forceRefresh = forceRefresh forceRefresh = forceRefresh
) )
} }
@ -231,6 +231,8 @@ class AttendancePresenter @Inject constructor(
}.sortedBy { item -> item.number } }.sortedBy { item -> item.number }
} }
.onResourceData { .onResourceData {
isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it)
view?.run { view?.run {
enableSwipe(true) enableSwipe(true)
showProgress(false) showProgress(false)
@ -238,6 +240,7 @@ class AttendancePresenter @Inject constructor(
showEmpty(it.isEmpty()) showEmpty(it.isEmpty())
showContent(it.isNotEmpty()) showContent(it.isNotEmpty())
updateData(it) updateData(it)
reloadNavigation()
} }
} }
.onResourceIntermediate { view?.showRefresh(true) } .onResourceIntermediate { view?.showRefresh(true) }
@ -263,6 +266,43 @@ class AttendancePresenter @Inject constructor(
.launch() .launch()
} }
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
if (initialDate == null) {
val lessons = attendanceRepository.getAttendance(
student = student,
semester = semester,
start = now().monday,
end = now().sunday,
forceRefresh = false,
).toFirstResult().dataOrNull.orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester)
}
if (currentDate == null) {
currentDate = initialDate
}
}
private fun isWeekendHasLessons(
lessons: List<Attendance>,
): Boolean = lessons.any {
it.date.dayOfWeek in listOf(
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY,
)
}
private fun getInitialDate(semester: Semester): LocalDate {
val now = now()
return when {
now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear)
isWeekendHasLessons -> now
else -> now.previousOrSameSchoolDay
}
}
private fun excuseAbsence(reason: String?, toExcuseList: List<Attendance>) { private fun excuseAbsence(reason: String?, toExcuseList: List<Attendance>) {
resourceFlow { resourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
@ -311,7 +351,7 @@ class AttendancePresenter @Inject constructor(
private fun reloadView(date: LocalDate) { private fun reloadView(date: LocalDate) {
currentDate = date currentDate = date
Timber.i("Reload attendance view with the date ${currentDate.toFormattedString()}") Timber.i("Reload attendance view with the date ${currentDate?.toFormattedString()}")
view?.apply { view?.apply {
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
@ -326,10 +366,13 @@ class AttendancePresenter @Inject constructor(
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun reloadNavigation() { private fun reloadNavigation() {
val currentDate = currentDate ?: return
view?.apply { view?.apply {
showPreButton(!currentDate.minusDays(1).isHolidays) showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays) showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise())
showNavigation(true)
} }
} }
} }

View File

@ -40,6 +40,8 @@ interface AttendanceView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showNavigation(show: Boolean)
fun showPreButton(show: Boolean) fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean) fun showNextButton(show: Boolean)

View File

@ -386,7 +386,7 @@ class DashboardPresenter @Inject constructor(
private fun loadLessons(student: Student, forceRefresh: Boolean) { private fun loadLessons(student: Student, forceRefresh: Boolean) {
flatResourceFlow { flatResourceFlow {
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay val date = LocalDate.now()
timetableRepository.getTimetable( timetableRepository.getTimetable(
student = student, student = student,

View File

@ -9,6 +9,7 @@ import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -160,6 +161,10 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
binding.timetableRecycler.visibility = if (show) VISIBLE else GONE binding.timetableRecycler.visibility = if (show) VISIBLE else GONE
} }
override fun showNavigation(show: Boolean) {
binding.timetableNavContainer.isVisible = true
}
override fun showPreButton(show: Boolean) { override fun showPreButton(show: Boolean) {
binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE
} }
@ -193,7 +198,9 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) presenter.currentDate?.toEpochDay()?.let {
outState.putLong(SAVED_DATE_KEY, it)
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -1,5 +1,8 @@
package io.github.wulkanowy.ui.modules.timetable package io.github.wulkanowy.ui.modules.timetable
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
@ -15,6 +18,8 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
@ -24,15 +29,16 @@ import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.isJustFinished import io.github.wulkanowy.utils.isJustFinished
import io.github.wulkanowy.utils.isShowTimeUntil import io.github.wulkanowy.utils.isShowTimeUntil
import io.github.wulkanowy.utils.left import io.github.wulkanowy.utils.left
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.until import io.github.wulkanowy.utils.until
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import java.time.DayOfWeek
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDate.now import java.time.LocalDate.now
@ -51,9 +57,10 @@ class TimetablePresenter @Inject constructor(
private val analytics: AnalyticsHelper, private val analytics: AnalyticsHelper,
) : BasePresenter<TimetableView>(errorHandler, studentRepository) { ) : BasePresenter<TimetableView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = now().nextOrSameSchoolDay private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
lateinit var currentDate: LocalDate var currentDate: LocalDate? = null
private set private set
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
@ -65,23 +72,30 @@ class TimetablePresenter @Inject constructor(
view.initView() view.initView()
Timber.i("Timetable was initialized") Timber.i("Timetable was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError errorHandler.showErrorMessage = ::showErrorViewOnError
reloadView(ofEpochDay(date ?: baseDate.toEpochDay())) currentDate = date?.let(::ofEpochDay)
loadData() loadData()
if (currentDate.isHolidays) setBaseDateOnHolidays()
} }
fun onPreviousDay() { fun onPreviousDay() {
reloadView(currentDate.previousSchoolDay) val date = if (isWeekendHasLessons) {
currentDate?.minusDays(1)
} else currentDate?.previousSchoolDay
reloadView(date ?: return)
loadData() loadData()
} }
fun onNextDay() { fun onNextDay() {
reloadView(currentDate.nextSchoolDay) val date = if (isWeekendHasLessons) {
currentDate?.plusDays(1)
} else currentDate?.nextSchoolDay
reloadView(date ?: return)
loadData() loadData()
} }
fun onPickDate() { fun onPickDate() {
view?.showDatePickerDialog(currentDate) view?.showDatePickerDialog(currentDate ?: return)
} }
fun onDateSet(year: Int, month: Int, day: Int) { fun onDateSet(year: Int, month: Int, day: Int) {
@ -110,10 +124,8 @@ class TimetablePresenter @Inject constructor(
Timber.i("Timetable view is reselected") Timber.i("Timetable view is reselected")
view?.let { view -> view?.let { view ->
if (view.currentStackSize == 1) { if (view.currentStackSize == 1) {
baseDate = now().nextOrSameSchoolDay if (currentDate != initialDate) {
reloadView(initialDate ?: return)
if (currentDate != baseDate) {
reloadView(baseDate)
loadData() loadData()
} else if (!view.isViewEmpty) { } else if (!view.isViewEmpty) {
view.resetView() view.resetView()
@ -134,34 +146,25 @@ class TimetablePresenter @Inject constructor(
return true return true
} }
private fun setBaseDateOnHolidays() {
flow {
val student = studentRepository.getCurrentStudent()
emit(semesterRepository.getCurrentSemester(student))
}.catch {
Timber.i("Loading semester result: An exception occurred")
}.onEach {
baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear)
currentDate = baseDate
reloadNavigation()
}.launch("holidays")
}
private fun loadData(forceRefresh: Boolean = false) { private fun loadData(forceRefresh: Boolean = false) {
flatResourceFlow { flatResourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester)
timetableRepository.getTimetable( timetableRepository.getTimetable(
student = student, student = student,
semester = semester, semester = semester,
start = currentDate, start = currentDate ?: now(),
end = currentDate, end = currentDate ?: now(),
forceRefresh = forceRefresh, forceRefresh = forceRefresh,
timetableType = TimetableRepository.TimetableType.NORMAL timetableType = TimetableRepository.TimetableType.NORMAL
) )
} }
.logResourceStatus("load timetable data") .logResourceStatus("load timetable data")
.onResourceData { .onResourceData {
isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it.lessons)
view?.run { view?.run {
enableSwipe(true) enableSwipe(true)
showProgress(false) showProgress(false)
@ -169,7 +172,8 @@ class TimetablePresenter @Inject constructor(
showContent(it.lessons.isNotEmpty()) showContent(it.lessons.isNotEmpty())
showEmpty(it.lessons.isEmpty()) showEmpty(it.lessons.isEmpty())
updateData(it.lessons) updateData(it.lessons)
setDayHeaderMessage(it.headers.singleOrNull { header -> header.date == currentDate }?.content) setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
reloadNavigation()
} }
} }
.onResourceIntermediate { view?.showRefresh(true) } .onResourceIntermediate { view?.showRefresh(true) }
@ -191,6 +195,44 @@ class TimetablePresenter @Inject constructor(
.launch() .launch()
} }
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
if (initialDate == null) {
val lessons = timetableRepository.getTimetable(
student = student,
semester = semester,
start = now().monday,
end = now().sunday,
forceRefresh = false,
timetableType = TimetableRepository.TimetableType.NORMAL
).toFirstResult().dataOrNull?.lessons.orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester)
}
if (currentDate == null) {
currentDate = initialDate
}
}
private fun isWeekendHasLessons(
lessons: List<Timetable>,
): Boolean = lessons.any {
it.date.dayOfWeek in listOf(
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY,
)
}
private fun getInitialDate(semester: Semester): LocalDate {
val now = now()
return when {
now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear)
isWeekendHasLessons -> now
else -> now.nextOrSameSchoolDay
}
}
private fun updateData(lessons: List<Timetable>) { private fun updateData(lessons: List<Timetable>) {
tickTimer?.cancel() tickTimer?.cancel()
@ -285,7 +327,7 @@ class TimetablePresenter @Inject constructor(
private fun reloadView(date: LocalDate) { private fun reloadView(date: LocalDate) {
currentDate = date currentDate = date
Timber.i("Reload timetable view with the date ${currentDate.toFormattedString()}") Timber.i("Reload timetable view with the date ${currentDate?.toFormattedString()}")
view?.apply { view?.apply {
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
@ -298,10 +340,13 @@ class TimetablePresenter @Inject constructor(
} }
private fun reloadNavigation() { private fun reloadNavigation() {
val currentDate = currentDate ?: return
view?.apply { view?.apply {
showPreButton(!currentDate.minusDays(1).isHolidays) showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays) showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise())
showNavigation(true)
} }
} }

View File

@ -36,6 +36,8 @@ interface TimetableView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showNavigation(show: Boolean)
fun showPreButton(show: Boolean) fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean) fun showNextButton(show: Boolean)

View File

@ -128,7 +128,9 @@
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:gravity="center" android:gravity="center"
android:orientation="horizontal" android:orientation="horizontal"
tools:ignore="UnusedAttribute"> android:visibility="gone"
tools:ignore="UnusedAttribute"
tools:visibility="visible">
<ImageButton <ImageButton
android:id="@+id/attendancePreviousButton" android:id="@+id/attendancePreviousButton"

View File

@ -128,7 +128,9 @@
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:gravity="center" android:gravity="center"
android:orientation="horizontal" android:orientation="horizontal"
tools:ignore="UnusedAttribute"> android:visibility="gone"
tools:ignore="UnusedAttribute"
tools:visibility="visible">
<ImageButton <ImageButton
android:id="@+id/timetablePreviousButton" android:id="@+id/timetablePreviousButton"