Merge pull request #3 from HabitRPG/hafiz/circular-progress-view

Stats Round WearOS Progress indicators UI
This commit is contained in:
Phillip Thelen 2022-06-22 10:33:34 +02:00 committed by GitHub
commit 3d6949e2cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 362 additions and 5 deletions

View file

@ -56,6 +56,7 @@ dependencies {
implementation("com.squareup.retrofit2:converter-moshi:$retrofit_version")
implementation("com.squareup.moshi:moshi-kotlin:$moshi_version")
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
kapt("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version")
//Analytics

View file

@ -0,0 +1,15 @@
package com.habitrpg.wearos.habitica.extensions
import android.view.View
import android.view.ViewTreeObserver
inline fun View.waitForLayout(crossinline f: View.() -> Unit) = with(viewTreeObserver) {
addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
})
}

View file

@ -1,17 +1,70 @@
package com.habitrpg.wearos.habitica.ui.activities
import android.os.Bundle
import android.view.View
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.ActivityStatsBinding
import com.habitrpg.android.habitica.extensions.observeOnce
import com.habitrpg.common.habitica.views.HabiticaIconsHelper
import com.habitrpg.wearos.habitica.extensions.waitForLayout
import com.habitrpg.wearos.habitica.models.Stats
import com.habitrpg.wearos.habitica.models.User
import com.habitrpg.wearos.habitica.ui.viewmodels.StatsViewModel
import com.habitrpg.wearos.habitica.ui.views.StatValue
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class StatsActivity: BaseActivity<ActivityStatsBinding, StatsViewModel>() {
class StatsActivity : BaseActivity<ActivityStatsBinding, StatsViewModel>() {
override val viewModel: StatsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityStatsBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
setStatViews()
viewModel.user.observe(this) {
updateStats(it)
}
}
}
private fun setStatViews() {
binding.hpStatValue.setStatValueResources(HabiticaIconsHelper.imageOfHeartLightBg(), R.color.hp_bar_color)
binding.expStatValue.setStatValueResources(HabiticaIconsHelper.imageOfExperience(), R.color.exp_bar_color)
binding.mpStatValue.setStatValueResources(HabiticaIconsHelper.imageOfMagic(), R.color.mpColor)
}
private fun updateStats(user: User) {
val stats = user.stats
stats?.let { updateBarViews(it) }
stats?.let { updateStatViews(it) }
}
private fun updateBarViews(stats: Stats) {
binding.hpBar.setPercentageValues(stats.hp?.toInt() ?: 0, stats.maxHealth ?: 0)
binding.hpBar.animateProgress()
binding.expBar.setPercentageValues(stats.exp?.toInt() ?: 0, stats.toNextLevel ?: 0)
binding.expBar.animateProgress()
if (stats.lvl ?: 0 < 10) {
binding.mpBar.visibility = View.GONE
} else {
binding.mpBar.setPercentageValues(stats.mp?.toInt() ?: 0, stats.maxMP ?: 0)
binding.mpBar.animateProgress()
}
}
private fun updateStatViews(stats: Stats) {
binding.hpStatValue.setStatValue(stats.maxHealth ?: 0, stats.hp?.toInt() ?: 0)
binding.expStatValue.setStatValue(stats.toNextLevel ?: 0, stats.exp?.toInt() ?: 0)
if (stats.lvl ?: 0 < 10) {
binding.mpStatValue.visibility = View.GONE
} else {
binding.mpStatValue.setStatValue(stats.maxMP ?: 0, stats.mp?.toInt() ?: 0)
}
}
}

View file

@ -1,6 +1,9 @@
package com.habitrpg.wearos.habitica.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import com.habitrpg.wearos.habitica.data.repositories.UserRepository
import com.habitrpg.wearos.habitica.models.User
import com.habitrpg.wearos.habitica.util.ExceptionHandlerBuilder
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@ -10,4 +13,6 @@ class StatsViewModel @Inject constructor(userRepository: UserRepository,
exceptionBuilder: ExceptionHandlerBuilder
) : BaseViewModel(userRepository, exceptionBuilder) {
var user: LiveData<User> = userRepository.getUser().asLiveData()
}

View file

@ -0,0 +1,127 @@
package com.habitrpg.wearos.habitica.ui.views
import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import com.habitrpg.android.habitica.R
class CircularProgressView(
context: Context?,
attrs: AttributeSet?
) : View(context, attrs) {
private val ovalSpace = RectF()
private var ovalSize = (resources.displayMetrics.heightPixels / 2)
private var currentPercentage = 55
private var PERCENTAGE_DIVIDER = 180
private val ARC_FULL_ROTATION_DEGREE = 360
val attributes = context?.theme?.obtainStyledAttributes(
attrs,
R.styleable.CircularProgressView,
0, 0
)
private val offset = attributes?.getInt(R.styleable.CircularProgressView_offset, 0)
private val backgroundArcColor = attributes?.getColor(R.styleable.CircularProgressView_backgroundArcColor, 0) ?: Color.GRAY
private var fillArcColor = attributes?.getColor(R.styleable.CircularProgressView_arcFillColor, 0) ?: Color.GRAY
private val parentArcPaint = Paint().apply {
style = Paint.Style.STROKE
isAntiAlias = true
color = backgroundArcColor
strokeWidth = 10f
}
private var fillArcPaint = Paint().apply {
style = Paint.Style.STROKE
isAntiAlias = true
color = fillArcColor
strokeWidth = 10f
strokeCap = Paint.Cap.ROUND
}
override fun onDraw(canvas: Canvas?) {
setSpace()
canvas?.let {
drawBackgroundArc(it)
drawInnerArc(it)
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
offset?.let { ovalSize = (height / 2) - it }
invalidate()
}
private fun setSpace() {
val horizontalCenter = (width.div(2)).toFloat()
val verticalCenter = (height.div(2)).toFloat()
val ovalSize = this.ovalSize
ovalSpace.set(
horizontalCenter - ovalSize,
verticalCenter - ovalSize,
horizontalCenter + ovalSize,
verticalCenter + ovalSize
)
}
private fun drawBackgroundArc(it: Canvas) {
it.drawArc(ovalSpace, 0f, 360f, false, parentArcPaint)
}
private fun drawInnerArc(canvas: Canvas) {
val percentageToFill = getCurrentAngleToFill()
canvas.drawArc(ovalSpace, 270f, percentageToFill, false, fillArcPaint)
}
fun setBarColor(barColor: Int) {
fillArcColor = context?.resources?.getColor(barColor, null) ?: backgroundArcColor
fillArcPaint = Paint().apply {
style = Paint.Style.STROKE
isAntiAlias = true
color = fillArcColor
strokeWidth = 10f
strokeCap = Paint.Cap.ROUND
}
}
fun setPercentageValues(currentValue: Int, maxValue: Int) {
currentPercentage = currentValue
PERCENTAGE_DIVIDER = maxValue
}
fun animateProgress() {
val currentPercent: Int = currentPercentage
val valuesHolder = PropertyValuesHolder.ofFloat(
PERCENTAGE_VALUE_HOLDER,
1f,
currentPercent.toFloat()
)
val animator = ValueAnimator().apply {
setValues(valuesHolder)
duration = 1000
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener {
val percentage = it.getAnimatedValue(PERCENTAGE_VALUE_HOLDER) as Float
currentPercentage = percentage.toInt()
invalidate()
}
}
animator.start()
}
companion object {
const val PERCENTAGE_VALUE_HOLDER = "percentage"
}
private fun getCurrentAngleToFill() = if(currentPercentage > 0) {(ARC_FULL_ROTATION_DEGREE.toFloat() * (currentPercentage.toFloat() / PERCENTAGE_DIVIDER.toFloat()))} else {1f}
}

View file

@ -0,0 +1,34 @@
package com.habitrpg.wearos.habitica.ui.views
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import com.habitrpg.android.habitica.databinding.StatValueLayoutBinding
import com.habitrpg.common.habitica.extensions.layoutInflater
class StatValue @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
ConstraintLayout(
context,
attrs,
defStyle
) {
var binding = StatValueLayoutBinding.inflate(context.layoutInflater, this)
fun setStatValue(maxValue: Int, currentValue: Int) {
binding.currentValue.text = currentValue.toString()
binding.maxValue.text = "/$maxValue"
invalidate()
}
fun setStatValueResources(bitmap: Bitmap, bitmapColor: Int) {
binding.bitmap.setImageBitmap(bitmap)
binding.currentValue.setTextColor(
context?.resources?.getColor(bitmapColor, null) ?: Color.WHITE
)
}
}

Binary file not shown.

View file

@ -1,7 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
<com.habitrpg.wearos.habitica.ui.views.CircularProgressView
android:id="@+id/hp_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:arcFillColor="@color/hp_bar_color"
app:backgroundArcColor="@color/bar_background_color"
app:offset="10" />
<com.habitrpg.wearos.habitica.ui.views.CircularProgressView
android:id="@+id/exp_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:arcFillColor="@color/exp_bar_color"
app:backgroundArcColor="@color/bar_background_color"
app:offset="28" />
<com.habitrpg.wearos.habitica.ui.views.CircularProgressView
android:id="@+id/mp_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:arcFillColor="@color/mp_bar_color"
app:backgroundArcColor="@color/bar_background_color"
app:offset="46" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="16dp"
android:orientation="vertical">
<com.habitrpg.wearos.habitica.ui.views.StatValue
android:id="@+id/hp_stat_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.habitrpg.wearos.habitica.ui.views.StatValue
android:id="@+id/exp_stat_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.habitrpg.wearos.habitica.ui.views.StatValue
android:id="@+id/mp_stat_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</RelativeLayout>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
tools:parentTag="com.habitrpg.wearos.habitica.ui.views.StatValue">
<View
android:id="@+id/view"
android:layout_width="1dp"
android:layout_height="1dp"
android:layout_marginStart="35dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/bitmap"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginBottom="2dp"
app:layout_constraintBottom_toBottomOf="@+id/current_value"
app:layout_constraintEnd_toStartOf="@+id/current_value"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/current_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
android:autoSizeMaxTextSize="18sp"
android:autoSizeMinTextSize="12sp"
android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform"
android:fontFamily="@font/press_start_reg"
android:text="00"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/view"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/max_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:autoSizeMaxTextSize="6sp"
android:autoSizeMinTextSize="4sp"
android:autoSizeTextType="uniform"
android:fontFamily="@font/press_start_reg"
android:text="/00"
android:textColor="@color/white"
android:textSize="6sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/view"
app:layout_constraintTop_toTopOf="parent" />
</merge>

View file

@ -4,4 +4,9 @@
<attr name="drawable" format="integer" />
<attr name="drawFromTop" format="boolean" />
</declare-styleable>
<declare-styleable name="CircularProgressView">
<attr name="offset" format="integer" />
<attr name="backgroundArcColor" format="color" />
<attr name="arcFillColor" format="color" />
</declare-styleable>
</resources>

View file

@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">@color/brand_300</color>
<color name="bar_background_color">#34313a</color>
<color name="exp_bar_color">@color/yellow_100</color>
<color name="mp_bar_color">#a9dcf6</color>
<color name="hp_bar_color">#f2666a</color>
</resources>