diff --git a/uhabits-android/build.gradle.kts b/uhabits-android/build.gradle.kts index 8e6d9947b..5e9adb694 100644 --- a/uhabits-android/build.gradle.kts +++ b/uhabits-android/build.gradle.kts @@ -115,6 +115,7 @@ dependencies { implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("com.google.android.material:material:1.8.0") implementation("com.opencsv:opencsv:5.7.1") + implementation("nl.dionsegijn:konfetti-xml:2.0.2") implementation(project(":uhabits-core")) kapt("com.google.dagger:dagger-compiler:$daggerVersion") kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion") diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt index 5df8ffd7b..bc1e8e4cb 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt @@ -24,6 +24,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE +import android.widget.LinearLayout import androidx.appcompat.app.AppCompatDialogFragment import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R @@ -34,6 +35,7 @@ import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.databinding.CheckmarkPopupBinding import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome import org.isoron.uhabits.utils.sres +import org.isoron.uhabits.utils.showConfetti class CheckmarkDialog : AppCompatDialogFragment() { var onToggle: (Int, String) -> Unit = { _, _ -> } @@ -64,6 +66,10 @@ class CheckmarkDialog : AppCompatDialogFragment() { val notes = view.notes.text.toString().trim() onToggle(v, notes) requireDialog().dismiss() + val konfettiView = requireActivity().findViewById(R.id.konfettiLayout) + when (v) { + YES_MANUAL -> showConfetti(konfettiView) + } } view.yesBtn.setOnClickListener { onClick(YES_MANUAL) } view.noBtn.setOnClickListener { onClick(NO) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt index 7e10baa51..0890e3176 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt @@ -7,6 +7,7 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.MotionEvent import android.view.View +import android.widget.LinearLayout import androidx.appcompat.app.AppCompatDialogFragment import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R @@ -14,6 +15,7 @@ import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.databinding.CheckmarkPopupBinding import org.isoron.uhabits.utils.InterfaceUtils import org.isoron.uhabits.utils.requestFocusWithKeyboard +import org.isoron.uhabits.utils.showConfetti import org.isoron.uhabits.utils.sres import java.text.DecimalFormat import java.text.DecimalFormatSymbols @@ -100,5 +102,11 @@ class NumberDialog : AppCompatDialogFragment() { val notes = view.notes.text.toString() onToggle(value, notes) requireDialog().dismiss() + val v = requireActivity().findViewById(R.id.konfettiLayout) + + if (value > 0.0) { + showConfetti(v) + + } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt index faa13173a..52ab3bc94 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt @@ -44,6 +44,7 @@ import org.isoron.uhabits.utils.addAtBottom import org.isoron.uhabits.utils.addAtTop import org.isoron.uhabits.utils.addBelow import org.isoron.uhabits.utils.buildToolbar +import org.isoron.uhabits.utils.buildKonfettiView import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dp @@ -69,6 +70,7 @@ class ListHabitsRootView @Inject constructor( val listView: HabitCardListView = habitCardListViewFactory.create() val llEmpty = EmptyListView(context) val tbar = buildToolbar() + val konfettiView = buildKonfettiView() val progressBar = TaskProgressBar(context, runner) val hintView: HintView val header = HeaderView(context, preferences, midnightTimer) @@ -80,6 +82,7 @@ class ListHabitsRootView @Inject constructor( val rootView = RelativeLayout(context).apply { background = sres.getDrawable(R.attr.windowBackgroundColor) + addAtTop(konfettiView) addAtTop(tbar) addBelow(header, tbar) addBelow(listView, header, height = MATCH_PARENT) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt index 39faa0283..891abb78d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt @@ -39,6 +39,7 @@ import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.utils.drawNotesIndicator import org.isoron.uhabits.utils.getFontAwesome +import org.isoron.uhabits.utils.showConfetti import org.isoron.uhabits.utils.sp import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.toMeasureSpec @@ -88,7 +89,7 @@ class CheckmarkButtonView( setOnLongClickListener(this) } - fun performToggle() { + fun performToggle(v: View) { value = Entry.nextToggleValue( value = value, isSkipEnabled = preferences.isSkipEnabled, @@ -96,17 +97,20 @@ class CheckmarkButtonView( ) onToggle(value, notes) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + when (value) { + YES_MANUAL -> showConfetti(v.rootView) + } invalidate() } override fun onClick(v: View) { - if (preferences.isShortToggleEnabled) performToggle() + if (preferences.isShortToggleEnabled) performToggle(v) else onEdit() } override fun onLongClick(v: View): Boolean { if (preferences.isShortToggleEnabled) onEdit() - else performToggle() + else performToggle(v) return true } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt index 35a8cecbd..e6768f14b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt @@ -36,6 +36,7 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.WindowManager +import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM import android.widget.RelativeLayout.ALIGN_PARENT_TOP @@ -46,6 +47,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.content.FileProvider import com.google.android.material.snackbar.Snackbar +import nl.dionsegijn.konfetti.core.Party +import nl.dionsegijn.konfetti.core.Position +import nl.dionsegijn.konfetti.core.emitter.Emitter +import nl.dionsegijn.konfetti.xml.KonfettiView import org.isoron.platform.gui.toInt import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R @@ -53,6 +58,7 @@ import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.ui.views.Theme import java.io.File +import java.util.concurrent.TimeUnit fun RelativeLayout.addBelow( view: View, @@ -79,7 +85,9 @@ fun RelativeLayout.addAtBottom( view.layoutParams = RelativeLayout.LayoutParams(width, height).apply { addRule(ALIGN_PARENT_BOTTOM) } - view.id = View.generateViewId() + if (view.id == null) { + view.id = View.generateViewId() + } this.addView(view) } @@ -92,7 +100,10 @@ fun RelativeLayout.addAtTop( view.layoutParams = RelativeLayout.LayoutParams(width, height).apply { addRule(ALIGN_PARENT_TOP) } - view.id = View.generateViewId() + + if (view.id == null) { + view.id = View.generateViewId() + } this.addView(view) } @@ -101,6 +112,28 @@ fun ViewGroup.buildToolbar(): Toolbar { return inflater.inflate(R.layout.toolbar, null) as Toolbar } +fun ViewGroup.buildKonfettiView(): View { + val inflater = LayoutInflater.from(context) + return inflater.inflate(R.layout.konfetti, null) as View +} + +fun showConfetti(view: View) { + val viewId = R.id.konfettttiView + val linearLayout = view.findViewById(R.id.konfettiLayout) + val kv = view.findViewById(viewId) + linearLayout.bringToFront() + val party = Party( + speed = 0f, + maxSpeed = 32f, + damping = 0.9f, + spread = 360, + colors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def, 0x818181, 0x81a48c), + position = Position.Relative(0.5, 0.3), + emitter = Emitter(duration = 300, TimeUnit.MILLISECONDS).max(300) + ) + kv.start(party) +} + fun View.showMessage(msg: String) { try { val snackbar = Snackbar.make(this, msg, Snackbar.LENGTH_SHORT) diff --git a/uhabits-android/src/main/res/layout/konfetti.xml b/uhabits-android/src/main/res/layout/konfetti.xml new file mode 100644 index 000000000..49bd60daa --- /dev/null +++ b/uhabits-android/src/main/res/layout/konfetti.xml @@ -0,0 +1,29 @@ + + + + + + +