mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-20 20:59:00 +00:00
Merge pull request #3 from HabitRPG/hafiz/circular-progress-view
Stats Round WearOS Progress indicators UI
This commit is contained in:
commit
3d6949e2cc
11 changed files with 362 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
BIN
wearos/src/main/res/font/press_start_reg.ttf
Normal file
BIN
wearos/src/main/res/font/press_start_reg.ttf
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
63
wearos/src/main/res/layout/stat_value_layout.xml
Normal file
63
wearos/src/main/res/layout/stat_value_layout.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue