From 7cc07ca511261f3d57308215bbbb4c84bbba7b95 Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Sat, 18 Dec 2021 15:34:32 +0100 Subject: [PATCH] ChargerDetailScreen: show large photo if supported #145 --- .../evmap/auto/ChargerDetailScreen.kt | 217 +++++++++++------- 1 file changed, 133 insertions(+), 84 deletions(-) diff --git a/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt index e1e1c791..fd4b855d 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt @@ -2,6 +2,7 @@ package net.vonforst.evmap.auto import android.content.Intent import android.graphics.Bitmap +import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.text.SpannableStringBuilder @@ -9,8 +10,10 @@ import android.text.Spanned import androidx.car.app.CarContext import androidx.car.app.CarToast import androidx.car.app.Screen +import androidx.car.app.constraints.ConstraintManager import androidx.car.app.model.* import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.scale import androidx.lifecycle.lifecycleScope import coil.imageLoader import coil.request.ImageRequest @@ -50,10 +53,17 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : private val referenceData = api.getReferenceData(lifecycleScope, carContext) private val imageSize = 128 // images should be 128dp according to docs + private val imageSizeLarge = 192 // this is not yet documented, just guessing private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = imageSize) + private val maxRows = if (ctx.carAppApiLevel >= 2) { + ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE) + } else 2 + private val largeImageSupported = + ctx.carAppApiLevel >= 4 // since API 4, Row.setImage is supported + init { referenceData.observe(this) { loadCharger() @@ -66,86 +76,21 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : return PaneTemplate.Builder( Pane.Builder().apply { charger?.let { charger -> - addRow(Row.Builder().apply { - setTitle(charger.address.toString()) - - val icon = iconGen.getBitmap( - tint = getMarkerTint(charger), - fault = charger.faultReport != null, - multi = charger.isMulti() - ) - setImage( - CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(), - Row.IMAGE_TYPE_LARGE - ) - - val chargepointsText = SpannableStringBuilder() - charger.chargepointsMerged.forEachIndexed { i, cp -> - if (i > 0) chargepointsText.append(" · ") - chargepointsText.append( - "${cp.count}× ${ - nameForPlugType( - carContext.stringProvider(), - cp.type + if (largeImageSupported && photo != null) { + setImage(CarIcon.Builder(IconCompat.createWithBitmap(photo)).build()) + } + generateRows(charger).forEach { addRow(it) } + addAction( + Action.Builder() + .setIcon( + CarIcon.Builder( + IconCompat.createWithResource( + carContext, + R.drawable.ic_navigation ) - } ${cp.formatPower()}" + ).build() ) - availability?.status?.get(cp)?.let { status -> - chargepointsText.append( - " (${availabilityText(status)}/${cp.count})", - ForegroundCarColorSpan.create(carAvailabilityColor(status)), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - } - addText(chargepointsText) - }.build()) - addRow(Row.Builder().apply { - photo?.let { - setImage( - CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(), - Row.IMAGE_TYPE_LARGE - ) - } - val operatorText = StringBuilder().apply { - charger.operator?.let { append(it) } - charger.network?.let { - if (isNotEmpty()) append(" · ") - append(it) - } - }.ifEmpty { - carContext.getString(R.string.unknown_operator) - } - setTitle(operatorText) - - charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) } - charger.faultReport?.created?.let { - addText( - carContext.getString( - R.string.auto_fault_report_date, - it.atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) - ) - ) - } - - /*val types = charger.chargepoints.map { it.type }.distinct() - if (types.size == 1) { - setImage( - CarIcon.of(IconCompat.createWithResource(carContext, iconForPlugType(types[0]))), - Row.IMAGE_TYPE_ICON) - }*/ - }.build()) - addAction(Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.ic_navigation - ) - ).build() - ) - .setTitle(carContext.getString(R.string.navigate)) + .setTitle(carContext.getString(R.string.navigate)) .setBackgroundColor(CarColor.PRIMARY) .setOnClickListener { navigateToCharger(charger) @@ -197,6 +142,85 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : }.build() } + private fun generateRows(charger: ChargeLocation): List { + val rows = mutableListOf() + + rows.add(Row.Builder().apply { + setTitle(charger.address.toString()) + + if (!largeImageSupported || photo == null) { + val icon = iconGen.getBitmap( + tint = getMarkerTint(charger), + fault = charger.faultReport != null, + multi = charger.isMulti() + ) + setImage( + CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(), + Row.IMAGE_TYPE_LARGE + ) + } + + val chargepointsText = SpannableStringBuilder() + charger.chargepointsMerged.forEachIndexed { i, cp -> + if (i > 0) chargepointsText.append(" · ") + chargepointsText.append( + "${cp.count}× ${ + nameForPlugType( + carContext.stringProvider(), + cp.type + ) + } ${cp.formatPower()}" + ) + availability?.status?.get(cp)?.let { status -> + chargepointsText.append( + " (${availabilityText(status)}/${cp.count})", + ForegroundCarColorSpan.create(carAvailabilityColor(status)), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + addText(chargepointsText) + }.build()) + rows.add(Row.Builder().apply { + if (photo != null && !largeImageSupported) { + setImage( + CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(), + Row.IMAGE_TYPE_LARGE + ) + } + val operatorText = StringBuilder().apply { + charger.operator?.let { append(it) } + charger.network?.let { + if (isNotEmpty()) append(" · ") + append(it) + } + }.ifEmpty { + carContext.getString(R.string.unknown_operator) + } + setTitle(operatorText) + + charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) } + charger.faultReport?.created?.let { + addText( + carContext.getString( + R.string.auto_fault_report_date, + it.atZone(ZoneId.systemDefault()) + .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + ) + ) + } + + /*val types = charger.chargepoints.map { it.type }.distinct() + if (types.size == 1) { + setImage( + CarIcon.of(IconCompat.createWithResource(carContext, iconForPlugType(types[0]))), + Row.IMAGE_TYPE_ICON) + }*/ + }.build()) + + return rows + } + private fun navigateToCharger(charger: ChargeLocation) { val coord = charger.coordinates val intent = @@ -212,18 +236,43 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : lifecycleScope.launch { val response = api.getChargepointDetail(referenceData, chargerSparse.id) if (response.status == Status.SUCCESS) { - charger = response.data!! + val charger = response.data!! - val photo = charger?.photos?.firstOrNull() + val photo = charger.photos?.firstOrNull() photo?.let { - val size = (carContext.resources.displayMetrics.density * 64).roundToInt() + val sizeDp = if (largeImageSupported) imageSizeLarge else imageSize + val size = (carContext.resources.displayMetrics.density * sizeDp).roundToInt() val url = photo.getUrl(size = size) val request = ImageRequest.Builder(carContext).data(url).build() - this@ChargerDetailScreen.photo = + var img = (carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap - } - availability = charger?.let { getAvailability(it).data } + if (largeImageSupported) { + // draw icon on top of image + val icon = iconGen.getBitmap( + tint = getMarkerTint(charger), + fault = charger.faultReport != null, + multi = charger.isMulti() + ) + + img = img.copy(Bitmap.Config.ARGB_8888, true) + val iconSmall = icon.scale( + (img.height * 0.4 / icon.height * icon.width).roundToInt(), + (img.height * 0.4).roundToInt() + ) + val canvas = Canvas(img) + canvas.drawBitmap( + iconSmall, + 0f, + (img.height - iconSmall.height * 1.1).toFloat(), + null + ) + } + this@ChargerDetailScreen.photo = img + } + this@ChargerDetailScreen.charger = charger + + availability = getAvailability(charger).data invalidate() } else {