Skip to content

Commit

Permalink
Added expand animations, added charging status, added normalizing images
Browse files Browse the repository at this point in the history
  • Loading branch information
Ixam97 committed Apr 24, 2024
1 parent e1e88d2 commit 85d4d2c
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 20 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ android {
applicationId = "de.ixam97.carstatswidget"
minSdk = 24
targetSdk = 34
versionCode = 9
versionName = "0.3.0"
versionCode = 10
versionName = "0.3.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
32 changes: 30 additions & 2 deletions app/src/main/java/de/ixam97/carstatswidget/repository/CarData.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.ixam97.carstatswidget.repository

import android.content.Context
import android.graphics.Bitmap
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.Serializer
Expand All @@ -12,6 +13,7 @@ import kotlinx.serialization.json.Json
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.io.Serial

@Serializable
sealed interface CarDataStatus {
Expand All @@ -27,6 +29,32 @@ sealed interface CarDataStatus {
object ConfigChanged: CarDataStatus
}

@Serializable
sealed interface ChargingStatus {
@Serializable
object Charging: ChargingStatus
@Serializable
object Idle: ChargingStatus
@Serializable
object Done: ChargingStatus
@Serializable
object Undefined: ChargingStatus
@Serializable
class Unknown(val unknownStatusString: String): ChargingStatus
}

@Serializable
sealed interface ChargerConnectionStatus {
@Serializable
object Connected: ChargerConnectionStatus
@Serializable
object Disconnected: ChargerConnectionStatus
@Serializable
object Undefined: ChargerConnectionStatus
@Serializable
class Unknown(val unknownStatusString: String): ChargerConnectionStatus
}

@Serializable
data class CarDataInfo(
val status: CarDataStatus,
Expand All @@ -46,7 +74,7 @@ data class CarDataInfo(
val shortName: String = "",
val id: String,
val api: String,
val isCharging: Boolean? = null,
val isConnected: Boolean? = null,
val chargingStatus: ChargingStatus = ChargingStatus.Undefined,
val chargerConnectionStatus: ChargerConnectionStatus = ChargerConnectionStatus.Undefined
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ object PolestarRepository: CarDataInterface() {
vehicles.add(
CarDataInfo.CarData(
stateOfCharge = data.batteryChargeLevelPercentage,
chargerConnectionStatus = when (data.chargerConnectionStatus) {
"CHARGER_CONNECTION_STATUS_CONNECTED" -> ChargerConnectionStatus.Connected
"CHARGER_CONNECTION_STATUS_DISCONNECTED" -> ChargerConnectionStatus.Disconnected
else -> ChargerConnectionStatus.Unknown(data.chargerConnectionStatus)
},
chargingStatus = when (data.chargingStatus) {
"CHARGING_STATUS_IDLE" -> ChargingStatus.Idle
"CHARGING_STATUS_CHARGING" -> ChargingStatus.Charging
"CHARGING_STATUS_DONE" -> ChargingStatus.Done
else -> ChargingStatus.Unknown(data.chargingStatus)
},
lastSeen = DateFormat.getDateTimeInstance().format(lastSeenDate),
lastUpdated = DateFormat.getDateTimeInstance().format(Date(System.currentTimeMillis())),
imgUrl = car.content.images.studio.url + "&width=1400&angle=1&bg=00000000",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,15 @@ object TibberRepository: CarDataInterface() {
val localeTimeDateFormat = DateFormat.getDateTimeInstance()
val lastUpdateDate = Date(System.currentTimeMillis())

Log.i(TAG, data.toString())

for (car in data!!.data.me.homes[0].electricVehicles) {
val lastSeenDate = simpleDateFormat.parse(car.lastSeen)!!

vehicles.add(
CarDataInfo.CarData(
stateOfCharge = car.battery.percent,
chargingStatus = if (car.battery.isCharging) ChargingStatus.Charging else ChargingStatus.Idle,
lastSeen = localeTimeDateFormat.format(lastSeenDate),
lastUpdated = localeTimeDateFormat.format(lastUpdateDate),
imgUrl = car.imgUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ data class Home(

data class ElectricVehicle(
val battery: Battery,
val batteryText: String,
val chargingText: String,
val imgUrl: String,
val lastSeen: String,
val name: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package de.ixam97.carstatswidget.repository.tibberQuery

data class TibberQuery(
val query: String = "{me {homes {electricVehicles {id lastSeen name shortName battery {percent isCharging} imgUrl}}}}"
val query: String = "{me {homes {electricVehicles {id lastSeen name shortName battery {percent isCharging} batteryText chargingText imgUrl}}}}"
)
21 changes: 21 additions & 0 deletions app/src/main/java/de/ixam97/carstatswidget/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package de.ixam97.carstatswidget.ui

import android.app.Application
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
Expand All @@ -19,7 +22,10 @@ import de.ixam97.carstatswidget.repository.CarDataRepository
import de.ixam97.carstatswidget.repository.ApiCredentials
import de.ixam97.carstatswidget.repository.PolestarRepository
import de.ixam97.carstatswidget.util.AvailableApis
import de.ixam97.carstatswidget.util.ResizeBitmap
import de.ixam97.carstatswidget.util.SemanticVersion
import de.ixam97.carstatswidget.util.resize
import de.ixam97.carstatswidget.util.trimBorders
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -77,6 +83,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application = a
private val _networkState = MutableStateFlow<CarDataRepository.NetworkState>(CarDataRepository.NetworkState())
val networkState = _networkState.asStateFlow()

private val _carBitmapsState = MutableStateFlow<HashMap<String, Bitmap?>>(HashMap())
val carBitmapsState = _carBitmapsState.asStateFlow()

data class CarInfoState(
val carDataInfo: CarDataInfo = CarDataInfo(CarDataStatus.Unavailable, message = "No Data"),
val dataAvailable: Boolean = false,
Expand Down Expand Up @@ -216,6 +225,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application = a
}
}

fun addCarBitmap(drawable: Drawable, key: String) {
viewModelScope.launch {
if (!_carBitmapsState.value.contains(key)) {
val bitmaps = _carBitmapsState.value
bitmaps[key] = drawable.toBitmap().trimBorders(80f)
_carBitmapsState.update {
bitmaps
}
}
}
}

// fun polestarTest() {
// viewModelScope.launch {
// val credentials = ApiCredentials(
Expand Down
111 changes: 100 additions & 11 deletions app/src/main/java/de/ixam97/carstatswidget/ui/components/CarInfoCard.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package de.ixam97.carstatswidget.ui.components

import android.graphics.Bitmap
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
Expand All @@ -10,6 +15,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -18,9 +24,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.outlined.ExpandLess
import androidx.compose.material.icons.outlined.ExpandMore
Expand All @@ -45,19 +49,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ExperimentalMotionApi
import androidx.compose.ui.platform.LocalContext
import coil.compose.SubcomposeAsyncImage
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.size.Size
import de.ixam97.carstatswidget.R
import de.ixam97.carstatswidget.repository.CarDataInfo
import de.ixam97.carstatswidget.repository.ChargerConnectionStatus
import de.ixam97.carstatswidget.repository.ChargingStatus
import de.ixam97.carstatswidget.ui.MainViewModel

@Composable
Expand All @@ -78,7 +89,7 @@ fun CarInfoCard(viewModel: MainViewModel) {

// }
// for (cardData in cardDataState) {
CarCard(carData, index)
CarCard(carData, index, viewModel)
}
/*
when (carInfoState.carDataInfo) {
Expand Down Expand Up @@ -106,7 +117,7 @@ fun CarInfoCard(viewModel: MainViewModel) {
}

@Composable
fun CarCard(carData: CarDataInfo.CarData, index: Int) {
fun CarCard(carData: CarDataInfo.CarData, index: Int, viewModel: MainViewModel) {

var expanded by remember { mutableStateOf(false) }
// val progress: Float by animateFloatAsState(targetValue = if (expanded) 1f else 0f)
Expand All @@ -129,8 +140,10 @@ fun CarCard(carData: CarDataInfo.CarData, index: Int) {
) {
DynamicCarInfo(
carData = carData,
index = index,
expand = {expanded = !expanded},
expanded = expanded
expanded = expanded,
viewModel = viewModel
)
}
}
Expand Down Expand Up @@ -181,17 +194,36 @@ fun ErrorCard(

@OptIn(ExperimentalMotionApi::class)
@Composable
fun DynamicCarInfo(carData: CarDataInfo.CarData, expand: () -> Unit, expanded: Boolean) {
fun DynamicCarInfo(carData: CarDataInfo.CarData, index: Int, expand: () -> Unit, expanded: Boolean, viewModel: MainViewModel) {
// val ctx = LocalContext.current
// val motionScene = remember { ctx.resources.openRawResource(R.raw.motion_scene).readBytes().decodeToString() }

val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(carData.imgUrl)
.size(Size.ORIGINAL)
.crossfade(true)
.allowHardware(false)
.build(),
)

val carBitmaps by viewModel.carBitmapsState.collectAsState()

val imageState = painter.state
if (imageState is AsyncImagePainter.State.Success && !carBitmaps.contains(carData.id)) {
viewModel.addCarBitmap(imageState.result.drawable, carData.id)
Log.i("Bitmap", "Trimming car bitmap")
}

val carBitmap: Bitmap? = if (!carBitmaps.contains(carData.id)) null else carBitmaps[carData.id]

Column(
modifier = Modifier
.padding(16.dp, 0.dp)
.fillMaxWidth()
.wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(0.dp),
horizontalAlignment = Alignment.CenterHorizontally
// horizontalAlignment = Alignment.CenterHorizontally
) {

Row (
Expand Down Expand Up @@ -224,10 +256,17 @@ fun DynamicCarInfo(carData: CarDataInfo.CarData, expand: () -> Unit, expanded: B
)
} else Spacer(modifier = Modifier.size(10.dp))

var size by remember { mutableStateOf(IntSize.Zero) }

Row(modifier = Modifier
.wrapContentWidth(),
.fillMaxWidth()
.defaultMinSize(minHeight = 40.dp)
.onSizeChanged {
size = it
},
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
/*
SubcomposeAsyncImage(
modifier = Modifier
.height(if (expanded) 200.dp else 40.dp),
Expand All @@ -254,16 +293,66 @@ fun DynamicCarInfo(carData: CarDataInfo.CarData, expand: () -> Unit, expanded: B
modifier = Modifier.align(Alignment.Center)
)
}
)*/

val density = LocalDensity.current
val maxWidth = with(density) { size.width.toDp() }

val animatedHeight = animateDpAsState(
targetValue = if (expanded) 200.dp else 40.dp
)
val animatedWidth = animateDpAsState(
targetValue = if (expanded) maxWidth else 80.dp
)

val animatedPadding = animateDpAsState(
targetValue = if (expanded) 16.dp else 2.dp
)

if (!expanded) SocBar(soc = carData.stateOfCharge)
if (carBitmap != null) {
AnimatedVisibility(visible = true) {
Image(
modifier = Modifier
.height(animatedHeight.value)
.width(animatedWidth.value)
.padding(animatedPadding.value),
bitmap = carBitmap.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.Fit
)
}
} else {
CircularProgressIndicator(
modifier = Modifier
.padding(if (expanded) 24.dp else 0.dp)
.width(40.dp)
.height(40.dp)
.padding(if (expanded) 0.dp else 8.dp)
)
}

if (!expanded) SocBar(
modifier = Modifier
.defaultMinSize(minWidth = maxWidth - 80.dp),
soc = carData.stateOfCharge
)
}
if (expanded) {
Column(Modifier.fillMaxWidth()) {
Column(Modifier.fillMaxWidth().animateContentSize()) {
SocBar(soc = carData.stateOfCharge)
Spacer(modifier = Modifier.size(8.dp))
Text(text = stringResource(R.string.last_seen_label, carData.lastSeen))
Text(text = stringResource(R.string.last_request_label, carData.lastUpdated))
if (carData.chargingStatus != ChargingStatus.Undefined) {
Text(text = "Charging Status: " + (carData.chargingStatus.javaClass.kotlin.simpleName?:"Unknown"))
} else if (carData.chargingStatus is ChargingStatus.Unknown) {
Text(text = "Unknown Charging Status: ${carData.chargingStatus.unknownStatusString}")
}
if (carData.chargerConnectionStatus != ChargerConnectionStatus.Undefined) {
Text(text = "Charger Connection Status: " + (carData.chargerConnectionStatus.javaClass.kotlin.simpleName?:"Unknown"))
} else if (carData.chargerConnectionStatus is ChargerConnectionStatus.Unknown) {
Text(text = "Unknown Charging Status: ${carData.chargerConnectionStatus.unknownStatusString}")
}
}
}
Spacer(Modifier.size(16.dp))
Expand Down

0 comments on commit 85d4d2c

Please sign in to comment.