Skip to content
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 7 commits into from
Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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()
Copy link
Member

Choose a reason for hiding this comment

The 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(
Expand Down
Loading