Skip to content

Commit

Permalink
refactor: centralize route pill logic in shared code (#378)
Browse files Browse the repository at this point in the history
  • Loading branch information
boringcactus authored Sep 3, 2024
1 parent b0c871d commit c9dd7d6
Show file tree
Hide file tree
Showing 6 changed files with 556 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import com.mbta.tid.mbta_app.android.R
import com.mbta.tid.mbta_app.model.Route
import com.mbta.tid.mbta_app.model.RouteType

@Composable fun routeIcon(route: Route) = routeIcon(route.type)

@Composable
fun routeIcon(route: Route) =
when (route.type) {
fun routeIcon(routeType: RouteType) =
when (routeType) {
RouteType.BUS -> Pair(painterResource(id = R.drawable.mode_bus), "Bus")
RouteType.COMMUTER_RAIL -> Pair(painterResource(id = R.drawable.mode_cr), "Commuter Rail")
RouteType.FERRY -> Pair(painterResource(id = R.drawable.mode_ferry), "Ferry")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,63 +15,18 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mbta.tid.mbta_app.android.R
import com.mbta.tid.mbta_app.android.util.fromHex
import com.mbta.tid.mbta_app.model.Line
import com.mbta.tid.mbta_app.model.Route
import com.mbta.tid.mbta_app.model.RoutePillSpec
import com.mbta.tid.mbta_app.model.RouteType

enum class RoutePillType {
Fixed,
Flex
}

private sealed interface PillContent {
data class Text(val text: String) : PillContent

data class Image(val painter: Painter, val contentDescription: String) : PillContent
}

private fun linePillContent(line: Line, type: RoutePillType) =
if (line.longName == "Green Line" && type == RoutePillType.Fixed) PillContent.Text("GL")
else PillContent.Text(line.longName)

private fun lightRailPillContent(route: Route, type: RoutePillType) =
if (route.longName.startsWith("Green Line ")) {
if (type == RoutePillType.Fixed)
PillContent.Text(route.longName.replace("Green Line ", "GL "))
else PillContent.Text(route.shortName)
} else if (route.longName == "Mattapan Trolley" && type == RoutePillType.Fixed)
PillContent.Text("M")
else PillContent.Text(route.longName)

private fun heavyRailPillContent(route: Route, type: RoutePillType) =
if (type == RoutePillType.Fixed)
PillContent.Text(route.longName.split(" ").map { it.first() }.joinToString(separator = ""))
else PillContent.Text(route.longName)

private fun commuterRailPillContent(route: Route, type: RoutePillType) =
if (type == RoutePillType.Fixed) PillContent.Text("CR")
else PillContent.Text(route.longName.replace(" Line", ""))

@Composable
private fun busPillContent(route: Route, type: RoutePillType) =
if (route.id.startsWith("Shuttle") && type == RoutePillType.Fixed)
PillContent.Image(painterResource(R.drawable.mode_bus), route.shortName)
else PillContent.Text(route.shortName)

@Composable
private fun ferryPillContent(route: Route, type: RoutePillType) =
if (type == RoutePillType.Fixed)
PillContent.Image(painterResource(R.drawable.mode_ferry), route.longName)
else PillContent.Text(route.longName)
typealias RoutePillType = RoutePillSpec.Type

@Composable
fun RoutePill(
Expand All @@ -81,40 +36,23 @@ fun RoutePill(
isActive: Boolean = true,
modifier: Modifier = Modifier
) {
val (textColor, routeColor) =
when {
route == null ->
when {
line == null -> return
else -> Pair(Color.fromHex(line.textColor), Color.fromHex(line.color))
}
route.id.startsWith("Shuttle") && line != null ->
Pair(Color.fromHex(line.textColor), Color.fromHex(line.color))
else -> Pair(Color.fromHex(route.textColor), Color.fromHex(route.color))
}
val spec = RoutePillSpec(route, line, type)
val textColor = Color.fromHex(spec.textColor)
val routeColor = Color.fromHex(spec.routeColor)

val pillContent =
if (route == null) {
if (line == null) null else linePillContent(line, type)
} else
when (route.type) {
RouteType.LIGHT_RAIL -> lightRailPillContent(route, type)
RouteType.HEAVY_RAIL -> heavyRailPillContent(route, type)
RouteType.COMMUTER_RAIL -> commuterRailPillContent(route, type)
RouteType.BUS -> busPillContent(route, type)
RouteType.FERRY -> ferryPillContent(route, type)
}
val pillContent = spec.content

val isRectangle = route?.type == RouteType.BUS && !route.id.startsWith("Shuttle")
val shape = if (isRectangle) RoundedCornerShape(4.dp) else RoundedCornerShape(percent = 100)
val shape =
when (spec.shape) {
RoutePillSpec.Shape.Rectangle -> RoundedCornerShape(4.dp)
RoutePillSpec.Shape.Capsule -> RoundedCornerShape(percent = 100)
}

fun Modifier.withSizePadding() =
if (type == RoutePillType.Fixed) {
size(width = 50.dp, height = 24.dp)
} else if (route?.longName?.startsWith("Green Line") == true) {
size(24.dp)
} else {
height(24.dp).padding(horizontal = 12.dp)
when (spec.size) {
RoutePillSpec.Size.FixedPill -> size(width = 50.dp, height = 24.dp)
RoutePillSpec.Size.Circle -> size(24.dp)
RoutePillSpec.Size.FlexPill -> height(24.dp).padding(horizontal = 12.dp)
}

fun Modifier.withColor() =
Expand All @@ -127,8 +65,8 @@ fun RoutePill(
val finalModifier = modifier.withColor().withSizePadding()

when (pillContent) {
null -> {}
is PillContent.Text ->
RoutePillSpec.Content.Empty -> {}
is RoutePillSpec.Content.Text ->
Text(
pillContent.text.uppercase(),
modifier = finalModifier,
Expand All @@ -140,13 +78,15 @@ fun RoutePill(
lineHeight = 24.sp,
maxLines = 1
)
is PillContent.Image ->
is RoutePillSpec.Content.ModeImage -> {
val (painter, contentDescription) = routeIcon(routeType = pillContent.mode)
Icon(
painter = pillContent.painter,
contentDescription = pillContent.contentDescription,
painter = painter,
contentDescription = contentDescription,
modifier = finalModifier,
tint = if (isActive) textColor else LocalContentColor.current
)
}
}
}

Expand Down
6 changes: 5 additions & 1 deletion iosApp/iosApp/ComponentViews/RouteIcon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import shared
import SwiftUI

func routeIcon(_ route: Route) -> Image {
switch route.type {
routeIcon(route.type)
}

func routeIcon(_ routeType: RouteType) -> Image {
switch routeType {
case .bus:
Image(.modeBus)
case .commuterRail:
Expand Down
151 changes: 25 additions & 126 deletions iosApp/iosApp/ComponentViews/RoutePill.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,140 +10,40 @@ import shared
import SwiftUI

struct RoutePill: View {
enum `Type` {
case fixed
case flex
}

let route: Route?
let line: Line?
let type: `Type`
let type: RoutePillSpec.Type_
let isActive: Bool
let textColor: Color?
let routeColor: Color?
let textColor: Color
let routeColor: Color
let spec: RoutePillSpec

init(route: Route?, line: Line? = nil, type: Type, isActive: Bool = true) {
init(route: Route?, line: Line? = nil, type: RoutePillSpec.Type_, isActive: Bool = true) {
self.route = route
self.line = line
self.type = type
self.isActive = isActive
guard let route else {
guard let line else {
textColor = nil
routeColor = nil
return
}
textColor = Color(hex: line.textColor)
routeColor = Color(hex: line.color)
return
}
if route.id.starts(with: "Shuttle"), let line {
textColor = Color(hex: line.textColor)
routeColor = Color(hex: line.color)
} else {
textColor = Color(hex: route.textColor)
routeColor = Color(hex: route.color)
}
}

private enum PillContent {
case empty
case text(String)
case image(ImageResource)
}

private func getPillContent() -> PillContent {
guard let route else {
guard let line else {
return .empty
}
return Self.linePillContent(line: line, type: type)
}
return switch route.type {
case .lightRail: Self.lightRailPillContent(route: route, type: type)
case .heavyRail: Self.heavyRailPillContent(route: route, type: type)
case .commuterRail: Self.commuterRailPillContent(route: route, type: type)
case .bus: Self.busPillContent(route: route, type: type)
case .ferry: Self.ferryPillContent(route: route, type: type)
}
}

private static func linePillContent(line: Line, type: Type) -> PillContent {
if line.longName == "Green Line", type == .fixed {
.text("GL")
} else {
.text(line.longName)
}
}

private static func lightRailPillContent(route: Route, type: Type) -> PillContent {
if route.longName.starts(with: "Green Line ") {
if type == .fixed {
.text(route.longName.replacing("Green Line ", with: "GL "))
} else {
.text(route.shortName)
}
} else if route.longName == "Mattapan Trolley", type == .fixed {
.text("M")
} else {
.text(route.longName)
}
}

private static func heavyRailPillContent(route: Route, type: Type) -> PillContent {
if type == .fixed {
.text(String(route.longName.split(separator: " ").compactMap(\.first)))
} else {
.text(route.longName)
}
}

private static func commuterRailPillContent(route: Route, type: Type) -> PillContent {
if type == .fixed {
.text("CR")
} else {
.text(route.longName.replacing(" Line", with: ""))
}
}

private static func busPillContent(route: Route, type: Type) -> PillContent {
if route.id.starts(with: "Shuttle"), type == .fixed {
.image(.modeBus)
} else {
.text(route.shortName)
}
}

private static func ferryPillContent(route: Route, type: Type) -> PillContent {
if type == .fixed {
.image(.modeFerry)
} else {
.text(route.longName)
}
spec = .init(route: route, line: line, type: type)
textColor = .init(hex: spec.textColor)
routeColor = .init(hex: spec.routeColor)
}

@ViewBuilder func getPillBase() -> some View {
switch getPillContent() {
switch onEnum(of: spec.content) {
case .empty: EmptyView()
case let .text(text): Text(text)
case let .image(image): Image(image)
case let .text(text): Text(text.text)
case let .modeImage(mode): routeIcon(mode.mode)
}
}

private static func isRectangle(route: Route) -> Bool {
route.type == .bus && !route.id.starts(with: "Shuttle")
}

private struct FramePaddingModifier: ViewModifier {
let pill: RoutePill
let spec: RoutePillSpec

func body(content: Content) -> some View {
if pill.type == .fixed {
content.frame(width: 50, height: 24)
} else if pill.route?.longName.starts(with: "Green Line ") ?? false {
content.frame(width: 24, height: 24)
} else {
content.frame(height: 24).padding(.horizontal, 12)
switch spec.size {
case .fixedPill: content.frame(width: 50, height: 24)
case .circle: content.frame(width: 24, height: 24)
case .flexPill: content.frame(height: 24).padding(.horizontal, 12)
}
}
}
Expand All @@ -156,26 +56,25 @@ struct RoutePill: View {
content
.foregroundColor(pill.textColor)
.background(pill.routeColor)
} else if let route = pill.route, RoutePill.isRectangle(route: route) {
} else if pill.spec.shape == .rectangle {
content.overlay(
Rectangle().stroke(pill.routeColor ?? .deemphasized, lineWidth: 1).padding(1)
Rectangle().stroke(pill.routeColor, lineWidth: 1).padding(1)
)
} else {
content.overlay(
Capsule().stroke(pill.routeColor ?? .deemphasized, lineWidth: 1).padding(1)
Capsule().stroke(pill.routeColor, lineWidth: 1).padding(1)
)
}
}
}

private struct ClipShapeModifier: ViewModifier {
let pill: RoutePill
let spec: RoutePillSpec

func body(content: Content) -> some View {
if let route = pill.route, RoutePill.isRectangle(route: route) {
content.clipShape(Rectangle())
} else {
content.clipShape(Capsule())
switch spec.shape {
case .rectangle: content.clipShape(Rectangle())
case .capsule: content.clipShape(Capsule())
}
}
}
Expand All @@ -188,10 +87,10 @@ struct RoutePill: View {
.textCase(.uppercase)
.font(.custom("Helvetica Neue", size: 16).bold())
.tracking(0.5)
.modifier(FramePaddingModifier(pill: self))
.modifier(FramePaddingModifier(spec: spec))
.lineLimit(1)
.modifier(ColorModifier(pill: self))
.modifier(ClipShapeModifier(pill: self))
.modifier(ClipShapeModifier(spec: spec))
}
}
}
Expand Down
Loading

0 comments on commit c9dd7d6

Please sign in to comment.