Skip to content
Open
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
@@ -1,18 +1,22 @@
package com.skedgo.tripkit.ui.dialog

import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.TextUtils
import android.text.format.Time
import android.view.View
import android.view.View.OnClickListener
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import com.jakewharton.rxrelay2.PublishRelay
import com.skedgo.tripkit.ui.R
import kankan.wheel.widget.WheelView
import kankan.wheel.widget.adapters.ArrayWheelAdapter
import kankan.wheel.widget.adapters.DaysAdapter
import kankan.wheel.widget.adapters.NumericWheelAdapter
import java.lang.reflect.Field
import java.util.Calendar

class TimeDatePickerFragment : DialogFragment(), OnClickListener {
Expand Down Expand Up @@ -113,9 +117,25 @@ class TimeDatePickerFragment : DialogFragment(), OnClickListener {
mDaysView = view.findViewById<View>(R.id.daysView) as WheelView
val dayRange = 60
mDaysAdapter = DaysAdapter(this.activity, mCalendar, dayRange)
mDaysAdapter?.apply {
itemResource = R.layout.v4_view_wheel_time
itemTextResource = R.id.text
}
mDaysView?.viewAdapter = mDaysAdapter
mDaysView?.currentItem = (dayRange + 1) / 2

// Set shadow drawables for dark mode support
setWheelViewShadows(mDaysView)
setWheelViewShadows(mHoursView)
setWheelViewShadows(mMinutesView)
setWheelViewShadows(mAmPmView)

// Set center drawable and divider lines for dark mode support
setWheelViewCenterDrawable(mDaysView)
setWheelViewCenterDrawable(mHoursView)
setWheelViewCenterDrawable(mMinutesView)
setWheelViewCenterDrawable(mAmPmView)
Comment on lines +128 to +137
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @sg-jsonjuliane , just wanted to clarify, is this intended for dark mode only? If so, do we need a checker to detect whether the device is in dark mode? If not, then all good. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This applies to both. There are already separate day and night variants for the drawables being used, so they will adjust automatically.


return dialog
}

Expand Down Expand Up @@ -159,7 +179,77 @@ class TimeDatePickerFragment : DialogFragment(), OnClickListener {
dismiss()
}

/**
* Sets the top and bottom shadow drawables for a WheelView to support dark mode.
* Uses reflection to access private fields in the WheelView library.
*/
private fun setWheelViewShadows(wheelView: WheelView?) {
if (wheelView == null) return

try {
val context = requireContext()
val topShadow = ContextCompat.getDrawable(context, R.drawable.top_shadow)
val bottomShadow = ContextCompat.getDrawable(context, R.drawable.bottom_shadow)

if (topShadow == null || bottomShadow == null) return

// Get shadow fields directly
val topShadowField = wheelView.javaClass.getDeclaredField(WHEEL_VIEW_FIELD_TOP_SHADOW)
topShadowField.isAccessible = true

val bottomShadowField = wheelView.javaClass.getDeclaredField(WHEEL_VIEW_FIELD_BOTTOM_SHADOW)
bottomShadowField.isAccessible = true

// Set the shadow drawables
topShadowField.set(wheelView, topShadow)
bottomShadowField.set(wheelView, bottomShadow)

// Invalidate to refresh the view
wheelView.invalidate()
} catch (e: Exception) {
// Silently fail if reflection doesn't work (library might have changed)
e.printStackTrace()
}
}

/**
* Sets the divider lines for a WheelView to support dark mode.
* Uses reflection to access private fields in the WheelView library.
*/
private fun setWheelViewCenterDrawable(wheelView: WheelView?) {
if (wheelView == null) return

try {
val context = requireContext()
// Use @color/black which inverts to white in dark mode
val dividerColor = ContextCompat.getColor(context, R.color.black)
val lineDrawable = ColorDrawable(dividerColor)

// Get line fields directly
val topLineField = wheelView.javaClass.getDeclaredField(WHEEL_VIEW_FIELD_TOP_LINE)
topLineField.isAccessible = true

val bottomLineField = wheelView.javaClass.getDeclaredField(WHEEL_VIEW_FIELD_BOTTOM_LINE)
bottomLineField.isAccessible = true

// Set the drawables
topLineField.set(wheelView, lineDrawable)
bottomLineField.set(wheelView, lineDrawable)

// Invalidate to refresh the view
wheelView.invalidate()
} catch (e: Exception) {
// Silently fail if reflection doesn't work (library might have changed)
e.printStackTrace()
}
}

companion object {
// WheelView reflection field names
private const val WHEEL_VIEW_FIELD_TOP_SHADOW = "topShadow"
private const val WHEEL_VIEW_FIELD_BOTTOM_SHADOW = "bottomShadow"
private const val WHEEL_VIEW_FIELD_TOP_LINE = "topLine"
private const val WHEEL_VIEW_FIELD_BOTTOM_LINE = "bottomLine"
private const val ARG_TIME_TYPE = "timeType"
private const val ARG_INITIATOR_ID = "initiatorId"
private const val ARG_INITIAL_TIME = "initialTimeInMillis"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import android.graphics.Bitmap
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.util.Pair
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import com.skedgo.tripkit.ui.R
import com.skedgo.tripkit.ui.utils.isDarkMode
import kotlin.math.abs

class BearingMarkerIconBuilder(
Expand Down Expand Up @@ -160,10 +164,21 @@ class BearingMarkerIconBuilder(

val canvas = Canvas(vehiclePointerPinBitmap)

// Apply dark mode tinting to base (make it darker for dark mode)
val basePaint = if (mContext.resources.isDarkMode()) {
Paint().apply {
colorFilter = createDarkenColorFilter()
isAntiAlias = true
isFilterBitmap = true
}
} else {
null
}

// Locate the base
val baseLeft = (vehiclePointerBitmap.width - baseBitmap.width) / 2
val baseTop = vehiclePointerBitmap.height + padding
canvas.drawBitmap(baseBitmap, baseLeft.toFloat(), baseTop.toFloat(), null)
canvas.drawBitmap(baseBitmap, baseLeft.toFloat(), baseTop.toFloat(), basePaint)
canvas.drawBitmap(vehiclePointerBitmap, 0f, 0f, null)

return vehiclePointerPinBitmap
Expand All @@ -179,6 +194,17 @@ class BearingMarkerIconBuilder(

val canvas = Canvas(vehiclePointerBitmap)

// Apply dark mode tinting to pointer (make it darker for dark mode)
val paint = if (mContext.resources.isDarkMode()) {
Paint().apply {
colorFilter = createDarkenColorFilter()
isAntiAlias = true
isFilterBitmap = true
}
} else {
null
}

val rotateAngle = convertToCanvasAxes(mBearing)
if (mHasBearing) {
canvas.save()
Expand All @@ -189,11 +215,11 @@ class BearingMarkerIconBuilder(
(vehiclePointerBitmap.width / 2).toFloat(),
(vehiclePointerBitmap.height / 2).toFloat()
)
canvas.drawBitmap(pointerBitmap, 0f, 0f, mRotationPaint)
canvas.drawBitmap(pointerBitmap, 0f, 0f, paint ?: mRotationPaint)

canvas.restore()
} else {
canvas.drawBitmap(pointerBitmap, 0f, 0f, null)
canvas.drawBitmap(pointerBitmap, 0f, 0f, paint)
}

pointerBitmap.recycle()
Expand Down Expand Up @@ -265,4 +291,22 @@ class BearingMarkerIconBuilder(
drawable.draw(canvas)
return bitmap
}

/**
* Creates a ColorFilter that darkens white/light colors to medium-light gray for dark mode.
* This makes white pin markers visible on dark map backgrounds.
*/
private fun createDarkenColorFilter(): ColorMatrixColorFilter {
// Color matrix that converts white (#FFFFFF) to medium-light gray (#999999 / 60% gray)
// while preserving alpha channel
val colorMatrix = ColorMatrix(
floatArrayOf(
0.6f, 0f, 0f, 0f, 0f, // Red channel: reduce to 60% (153/255)
0f, 0.6f, 0f, 0f, 0f, // Green channel: reduce to 60% (153/255)
0f, 0f, 0.6f, 0f, 0f, // Blue channel: reduce to 60% (153/255)
0f, 0f, 0f, 1f, 0f // Alpha channel: keep as-is
)
)
return ColorMatrixColorFilter(colorMatrix)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.skedgo.tripkit.ui.search

import android.content.Context
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import androidx.databinding.ObservableField
Expand All @@ -12,7 +13,7 @@ import com.skedgo.tripkit.ui.utils.TapAction
import com.squareup.picasso.Picasso

sealed class SuggestionViewModel(
context: Context,
protected val context: Context,
val term: String? = null
) {
val icon: ObservableField<Drawable?> = ObservableField()
Expand All @@ -32,6 +33,37 @@ sealed class SuggestionViewModel(
abstract val onInfoClicked: TapAction<SuggestionViewModel>

abstract val onSuggestionActionClicked: TapAction<SuggestionViewModel>

/**
* Applies tint programmatically to icons for dark mode support.
*
* This approach is used because drawables are not uniform in format (some are PNGs which can't
* be easily themed with night-res) and not all can be supported with night-res theming. Some
* icons (like HOME and WORK) have circular backgrounds that would break if tinted, so we skip
* tinting for those. We'll audit and request proper night mode variants in the next dark mode
* support iteration.
*/
protected fun applyIconTintIfNeeded(drawable: Drawable?, id: Any?, locationType: Int?): Drawable? {
if (drawable == null) return null

// Skip tinting for HOME and WORK icons (they have circular backgrounds that would break with tinting)
// Check ID first (for FixedSuggestions from either package), then fall back to location type
val isHomeOrWork = if (id is Enum<*>) {
val enumName = id.name
enumName == "HOME" || enumName == "WORK"
} else {
false
} || locationType == Location.TYPE_HOME || locationType == Location.TYPE_WORK

if (isHomeOrWork) {
return drawable
}

// Apply tint for all other icons
val tintColor = ContextCompat.getColor(context, R.color.icon_tint_default)
drawable.mutate().setColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
return drawable
}
}

open class FixedSuggestionViewModel(
Expand All @@ -54,7 +86,10 @@ open class FixedSuggestionViewModel(
TapAction.create { this }

init {
icon.set(suggestion.icon())
val originalIcon = suggestion.icon()
val suggestionId = suggestion.id()
val locationType = suggestion.location()?.locationType
icon.set(applyIconTintIfNeeded(originalIcon, suggestionId, locationType))
}
}

Expand Down Expand Up @@ -217,13 +252,15 @@ class GoogleAndTripGoSuggestionViewModel(

when {
place.icon() != null -> {
icon.set(place.icon())
val placeIcon = place.icon()
icon.set(applyIconTintIfNeeded(placeIcon, null, location.locationType))
}
iconRes == 0 -> {
icon.set(null)
}
else -> {
icon.set(ContextCompat.getDrawable(context, iconRes))
val drawable = ContextCompat.getDrawable(context, iconRes)
icon.set(applyIconTintIfNeeded(drawable, null, location.locationType))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ open class GetTransportIconTintStrategy @Inject constructor(private val resource
Single.fromCallable {
when (resources.getBoolean(R.bool.trip_kit_use_service_color)) {
true -> ApplyTintStrategy
false -> NoTintStrategy
false -> ThemeAwareApplyTintStrategy(resources)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.skedgo.tripkit.ui.tripresults

import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Drawable
import com.skedgo.tripkit.ui.utils.isDarkMode
import com.skedgo.tripkit.ui.utils.tint
import com.skedgo.tripkit.routing.ServiceColor

/**
* Theme-aware tint strategy that applies white tint in dark mode when service colors are disabled.
* In light mode, returns drawable as-is. In dark mode, tints to white for visibility.
*/
class ThemeAwareApplyTintStrategy(private val resources: Resources) : TransportTintStrategy {

override fun apply(
remoteIconIsTemplate: Boolean,
remoteIconIsBranding: Boolean,
serviceColor: ServiceColor?,
drawable: Drawable
): Drawable {
// Don't tint branding icons (like Beam) - they should show original logo colors
if (remoteIconIsBranding) {
return drawable
}

if (resources.isDarkMode()) {
// Dark mode: tint to white for visibility
return drawable.tint(Color.WHITE)
}
// Light mode: return drawable as-is (no tint)
return drawable
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.skedgo.tripkit.ui.utils

import android.content.res.Configuration
import android.content.res.Resources

/**
* Checks if the app is currently in dark mode.
*/
fun Resources.isDarkMode(): Boolean {
val nightModeFlags = configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return nightModeFlags == Configuration.UI_MODE_NIGHT_YES
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<item>
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@android:color/white" />
<solid android:color="@color/white" />
</shape>
</item>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#212A33"
android:endColor="@android:color/transparent"
android:angle="90" />
</shape>

8 changes: 8 additions & 0 deletions TripKitAndroidUI/src/main/res/drawable-night/top_shadow.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#212A33"
android:endColor="@android:color/transparent"
android:angle="270" />
</shape>

Loading