-
Notifications
You must be signed in to change notification settings - Fork 189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Timetable hours #151
Merged
Merged
Timetable hours #151
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9aae41b
add hours layout
4c5623b
adjust timetable and hours layout
7eab101
spotlessApply
94ccf4d
removed timetable's hours from sessions list
a9f41ec
refactor
3c3e4c4
adjust timetable horizontal lines
f2bef4b
put the hours in the pager to fix scroll issues
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
240 changes: 240 additions & 0 deletions
240
feature-sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Hours.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
package io.github.droidkaigi.confsched2022.feature.sessions | ||
|
||
import androidx.compose.foundation.ExperimentalFoundationApi | ||
import androidx.compose.foundation.layout.width | ||
import androidx.compose.foundation.lazy.layout.LazyLayout | ||
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider | ||
import androidx.compose.material3.MaterialTheme | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.State | ||
import androidx.compose.runtime.derivedStateOf | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clipToBounds | ||
import androidx.compose.ui.draw.drawBehind | ||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.layout.Placeable | ||
import androidx.compose.ui.text.style.TextAlign | ||
import androidx.compose.ui.unit.Constraints | ||
import androidx.compose.ui.unit.Density | ||
import androidx.compose.ui.unit.dp | ||
|
||
@Composable | ||
fun HoursItem( | ||
modifier: Modifier = Modifier, | ||
hour: String | ||
) { | ||
Text( | ||
text = hour, | ||
color = Color.White, | ||
modifier = modifier, | ||
textAlign = TextAlign.Center | ||
) | ||
} | ||
|
||
@OptIn(ExperimentalFoundationApi::class) | ||
@Composable | ||
fun Hours( | ||
modifier: Modifier = Modifier, | ||
timetableState: TimetableState, | ||
content: @Composable (Modifier, String) -> Unit, | ||
) { | ||
val itemProvider = itemProvider({ hoursList.size }) { index -> | ||
content(modifier, hoursList[index]) | ||
} | ||
val density = timetableState.density | ||
val scrollState = timetableState.screenScrollState | ||
val hoursLayout = remember(hoursList) { | ||
HoursLayout( | ||
hours = hoursList, | ||
density = density, | ||
) | ||
} | ||
val hoursScreen = remember(hoursLayout, density) { | ||
HoursScreen( | ||
hoursLayout, | ||
scrollState, | ||
density | ||
) | ||
} | ||
val visibleItemLayouts by remember(hoursScreen) { hoursScreen.visibleItemLayouts } | ||
val lineColor = MaterialTheme.colorScheme.surfaceVariant | ||
val linePxSize = with(timetableState.density) { lineStrokeSize.toPx() } | ||
val lineOffset = with(density) { 67.dp.roundToPx() } | ||
val lineEnd = with(density) { hoursWidth.roundToPx() } | ||
LazyLayout( | ||
modifier = modifier | ||
.width(hoursWidth) | ||
.clipToBounds() | ||
.drawBehind { | ||
hoursScreen.timeHorizontalLines.value.forEach { | ||
drawLine( | ||
lineColor, | ||
Offset(lineOffset.toFloat(), it), | ||
Offset(lineEnd.toFloat(), it), | ||
linePxSize | ||
) | ||
} | ||
}, | ||
itemProvider = itemProvider | ||
) { constraint -> | ||
|
||
data class ItemData(val placeable: Placeable, val hoursItem: HoursItemLayout) | ||
hoursScreen.updateBounds(width = constraint.maxWidth, height = constraint.maxHeight) | ||
|
||
val items = visibleItemLayouts.map { (index, hoursLayout) -> | ||
ItemData( | ||
placeable = measure( | ||
index, | ||
Constraints.fixed( | ||
width = hoursLayout.width, | ||
height = hoursLayout.height | ||
) | ||
)[0], | ||
hoursItem = hoursLayout | ||
) | ||
} | ||
layout(constraint.maxWidth, constraint.maxHeight) { | ||
items.forEach { (placable, hoursLayout) -> | ||
placable.place( | ||
hoursLayout.left, | ||
hoursLayout.top + hoursScreen.scrollState.scrollY.toInt() | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private data class HoursLayout( | ||
val hours: List<String>, | ||
val density: Density, | ||
) { | ||
var hoursHeight = 0 | ||
var hoursWidth = 0 | ||
val minutePx = with(density) { (4.23).dp.roundToPx() } | ||
val hoursLayouts = hours.mapIndexed { index, it -> | ||
val hoursItemLayout = HoursItemLayout( | ||
index = index, | ||
density = density, | ||
minutePx = minutePx | ||
) | ||
hoursHeight = | ||
maxOf(hoursHeight, hoursItemLayout.bottom) | ||
hoursWidth = | ||
maxOf(hoursWidth, hoursItemLayout.width) | ||
hoursItemLayout | ||
} | ||
|
||
fun visibleItemLayouts( | ||
screenHeight: Int, | ||
scrollY: Int | ||
): List<IndexedValue<HoursItemLayout>> { | ||
return hoursLayouts.withIndex().filter { (_, layout) -> | ||
layout.isVisible(screenHeight, scrollY) | ||
} | ||
} | ||
} | ||
|
||
private data class HoursItemLayout( | ||
val density: Density, | ||
val minutePx: Int, | ||
val index: Int | ||
) { | ||
val topOffset = with(density) { horizontalLineTopOffset.roundToPx() } | ||
val itemOffset = with(density) { hoursItemTopOffset.roundToPx() } | ||
val height = minutePx * 60 | ||
val width = with(density) { hoursWidth.roundToPx() } | ||
val left = 0 | ||
val top = index * height + topOffset - itemOffset | ||
val right = left + width | ||
val bottom = top + height | ||
|
||
fun isVisible( | ||
screenHeight: Int, | ||
scrollY: Int | ||
): Boolean { | ||
val screenTop = -scrollY | ||
val screenBottom = -scrollY + screenHeight | ||
val yInside = | ||
top in screenTop..screenBottom || bottom in screenTop..screenBottom | ||
return yInside | ||
} | ||
} | ||
|
||
private class HoursScreen( | ||
val hoursLayout: HoursLayout, | ||
val scrollState: ScreenScrollState, | ||
density: Density, | ||
) { | ||
var width = 0 | ||
private set | ||
var height = 0 | ||
private set | ||
|
||
val visibleItemLayouts: State<List<IndexedValue<HoursItemLayout>>> = | ||
derivedStateOf { | ||
hoursLayout.visibleItemLayouts( | ||
height, | ||
scrollState.scrollY.toInt() | ||
) | ||
} | ||
|
||
val offset = with(density) { horizontalLineTopOffset.roundToPx() } | ||
val timeHorizontalLines = derivedStateOf { | ||
(0..10).map { | ||
scrollState.scrollY + hoursLayout.minutePx * 60 * it + offset | ||
} | ||
} | ||
|
||
override fun toString(): String { | ||
return "Screen(" + | ||
"width=$width, " + | ||
"height=$height, " + | ||
"scroll=$scrollState, " + | ||
"visibleItemLayouts=$visibleItemLayouts" + | ||
")" | ||
} | ||
|
||
fun updateBounds(width: Int, height: Int) { | ||
this.width = width | ||
this.height = height | ||
} | ||
} | ||
|
||
@OptIn(ExperimentalFoundationApi::class) | ||
private fun itemProvider( | ||
itemCount: () -> Int, | ||
itemContent: @Composable (Int) -> Unit | ||
): LazyLayoutItemProvider { | ||
return object : LazyLayoutItemProvider { | ||
@Composable | ||
override fun Item(index: Int) { | ||
itemContent(index) | ||
} | ||
|
||
override val itemCount: Int get() = itemCount() | ||
} | ||
} | ||
|
||
private val lineStrokeSize = 1.dp | ||
private val horizontalLineTopOffset = 16.dp | ||
private val hoursWidth = 75.dp | ||
private val hoursItemWidth = 43.dp | ||
private val hoursItemHeight = 24.dp | ||
private val hoursItemTopOffset = 11.dp | ||
private val hoursItemEndOffset = 16.dp | ||
private val hoursList = listOf( | ||
"10:00", | ||
"11:00", | ||
"12:00", | ||
"13:00", | ||
"14:00", | ||
"15:00", | ||
"16:00", | ||
"17:00", | ||
"18:00", | ||
"19:00", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package io.github.droidkaigi.confsched2022.feature.sessions | |
|
||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Row | ||
import androidx.compose.foundation.layout.fillMaxWidth | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.foundation.layout.padding | ||
|
@@ -34,6 +35,7 @@ import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTheme | |
import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTopAppBar | ||
import io.github.droidkaigi.confsched2022.feature.sessions.SessionsUiModel.ScheduleState | ||
import io.github.droidkaigi.confsched2022.feature.sessions.SessionsUiModel.ScheduleState.Loaded | ||
import io.github.droidkaigi.confsched2022.model.DroidKaigi2022Day | ||
import io.github.droidkaigi.confsched2022.model.DroidKaigiSchedule | ||
import io.github.droidkaigi.confsched2022.model.TimetableItemId | ||
import io.github.droidkaigi.confsched2022.model.fake | ||
|
@@ -100,38 +102,94 @@ fun Sessions( | |
CircularProgressIndicator() | ||
} else { | ||
val days = scheduleState.schedule.days | ||
HorizontalPager( | ||
count = days.size, | ||
state = pagerState | ||
) { dayIndex -> | ||
val day = days[dayIndex] | ||
val timetable = scheduleState.schedule.dayToTimetable[day].orEmptyContents() | ||
if (isTimetable) { | ||
Timetable(timetable) { timetableItem, isFavorited -> | ||
TimetableItem( | ||
timetableItem = timetableItem, | ||
isFavorited = isFavorited, | ||
modifier = Modifier | ||
.clickable( | ||
onClick = { onTimetableClick(timetableItem.id) } | ||
), | ||
) | ||
} | ||
} else { | ||
SessionList(timetable) { timetableItem, isFavorited -> | ||
SessionListItem( | ||
timetableItem = timetableItem, | ||
isFavorited = isFavorited, | ||
onFavoriteClick = onFavoriteClick | ||
) | ||
} | ||
} | ||
if (isTimetable) { | ||
Timetable( | ||
pagerState = pagerState, | ||
scheduleState = scheduleState, | ||
days = days, | ||
onTimetableClick = onTimetableClick | ||
) | ||
} else { | ||
SessionsList( | ||
pagerState = pagerState, | ||
scheduleState = scheduleState, | ||
days = days, | ||
onFavoriteClick = onFavoriteClick | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
@OptIn(ExperimentalPagerApi::class) | ||
@Composable | ||
fun Timetable( | ||
modifier: Modifier = Modifier, | ||
pagerState: PagerState, | ||
scheduleState: Loaded, | ||
days: Array<DroidKaigi2022Day>, | ||
onTimetableClick: (TimetableItemId) -> Unit, | ||
) { | ||
HorizontalPager( | ||
count = days.size, | ||
state = pagerState | ||
) { dayIndex -> | ||
val day = days[dayIndex] | ||
val timetable = scheduleState.schedule.dayToTimetable[day].orEmptyContents() | ||
val timetableState = rememberTimetableState() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💯 |
||
val coroutineScope = rememberCoroutineScope() | ||
|
||
Row(modifier = modifier) { | ||
Hours( | ||
timetableState = timetableState, | ||
modifier = modifier, | ||
) { modifier, hour -> | ||
HoursItem(hour = hour, modifier = modifier) | ||
} | ||
|
||
Timetable( | ||
timetable = timetable, | ||
timetableState = timetableState, | ||
coroutineScope, | ||
) { timetableItem, isFavorited -> | ||
TimetableItem( | ||
timetableItem = timetableItem, | ||
isFavorited = isFavorited, | ||
modifier = Modifier | ||
.clickable( | ||
onClick = { onTimetableClick(timetableItem.id) } | ||
), | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
@OptIn(ExperimentalPagerApi::class) | ||
@Composable | ||
fun SessionsList( | ||
pagerState: PagerState, | ||
scheduleState: Loaded, | ||
days: Array<DroidKaigi2022Day>, | ||
onFavoriteClick: (TimetableItemId, Boolean) -> Unit, | ||
) { | ||
HorizontalPager( | ||
count = days.size, | ||
state = pagerState | ||
) { dayIndex -> | ||
val day = days[dayIndex] | ||
val timetable = scheduleState.schedule.dayToTimetable[day].orEmptyContents() | ||
SessionList(timetable) { timetableItem, isFavorited -> | ||
SessionListItem( | ||
timetableItem = timetableItem, | ||
isFavorited = isFavorited, | ||
onFavoriteClick = onFavoriteClick | ||
) | ||
} | ||
} | ||
} | ||
|
||
@OptIn(ExperimentalPagerApi::class) | ||
@Composable | ||
fun SessionsTopBar( | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍