Can show habit group without interaction / scrolling

This commit is contained in:
Dharanish
2024-07-01 23:33:38 +02:00
parent af3283e52f
commit 506086f003
14 changed files with 478 additions and 133 deletions

View File

@@ -53,7 +53,7 @@ class HabitCardViewTest : BaseViewTest() {
.getByInterval(today.minus(300), today)
.map { it.value }.toIntArray()
view = component.getHabitCardViewFactory().create().apply {
view = component.getHabitCardViewFactory().createHabitCard().apply {
habit = habit1
values = entries
score = habit1.scores[today].value

View File

@@ -148,7 +148,7 @@ class ListHabitsRootView @Inject constructor(
private fun updateEmptyView() {
if (listAdapter.itemCount == 0) {
if (listAdapter.hasNoHabit()) {
if (listAdapter.hasNoHabit() && listAdapter.hasNoHabitGroup()) {
llEmpty.showEmpty()
} else {
llEmpty.showDone()

View File

@@ -82,7 +82,7 @@ class ListHabitsSelectionMenu @Inject constructor(
itemArchive.isVisible = behavior.canArchive()
itemUnarchive.isVisible = behavior.canUnarchive()
itemNotify.isVisible = prefs.isDeveloper
activeActionMode?.title = listAdapter.selected.size.toString()
activeActionMode?.title = listAdapter.selectedHabits.size.toString()
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
@@ -117,7 +117,7 @@ class ListHabitsSelectionMenu @Inject constructor(
}
R.id.action_notify -> {
for (h in listAdapter.selected)
for (h in listAdapter.selectedHabits)
notificationTray.show(h, DateUtils.getToday(), 0)
return true
}

View File

@@ -0,0 +1,72 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.TextPaint
import android.view.View
import android.view.View.MeasureSpec.EXACTLY
import org.isoron.uhabits.R
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.sp
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec
class AddButtonView(
context: Context
) : View(context),
View.OnClickListener {
var onEdit: () -> Unit = { }
private var drawer = Drawer()
init {
setOnClickListener(this)
}
override fun onClick(v: View) {
onEdit()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawer.draw(canvas)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight)
val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth)
super.onMeasure(
width.toMeasureSpec(EXACTLY),
height.toMeasureSpec(EXACTLY)
)
}
private inner class Drawer {
private val rect = RectF()
private val highContrastColor = sres.getColor(R.attr.contrast100)
private val paint = TextPaint().apply {
typeface = getFontAwesome()
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
fun draw(canvas: Canvas) {
paint.color = highContrastColor
val id = R.string.fa_plus
paint.textSize = sp(12.0f)
paint.strokeWidth = 0f
paint.style = Paint.Style.FILL
val label = resources.getString(id)
val em = paint.measureText("m")
rect.set(0f, 0f, width.toFloat(), height.toFloat())
rect.offset(0f, 0.4f * em)
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
}
}
}

View File

@@ -19,9 +19,10 @@
package org.isoron.uhabits.activities.habits.list.views
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
import org.isoron.uhabits.activities.habits.list.MAX_CHECKMARK_COUNT
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.models.ModelObservable
@@ -32,6 +33,7 @@ import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBeh
import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.inject.ActivityScope
import java.util.LinkedList
import java.util.UUID
import javax.inject.Inject
/**
@@ -46,14 +48,16 @@ class HabitCardListAdapter @Inject constructor(
private val cache: HabitCardListCache,
private val preferences: Preferences,
private val midnightTimer: MidnightTimer
) : RecyclerView.Adapter<HabitCardViewHolder?>(),
) : Adapter<HabitCardViewHolder?>(),
HabitCardListCache.Listener,
MidnightTimer.MidnightListener,
ListHabitsMenuBehavior.Adapter,
ListHabitsSelectionMenuBehavior.Adapter {
val observable: ModelObservable = ModelObservable()
private var listView: HabitCardListView? = null
val selected: LinkedList<Habit> = LinkedList()
val selectedHabits: LinkedList<Habit> = LinkedList()
val selectedHabitGroups: LinkedList<HabitGroup> = LinkedList()
override fun atMidnight() {
cache.refreshAllHabits()
}
@@ -66,17 +70,25 @@ class HabitCardListAdapter @Inject constructor(
return cache.hasNoHabit()
}
fun hasNoHabitGroup(): Boolean {
return cache.hasNoHabitGroup()
}
/**
* Sets all items as not selected.
*/
override fun clearSelection() {
selected.clear()
selectedHabits.clear()
notifyDataSetChanged()
observable.notifyListeners()
}
override fun getSelected(): List<Habit> {
return ArrayList(selected)
return ArrayList(selectedHabits)
}
override fun getSelectedHabitGroups(): List<HabitGroup> {
return ArrayList(selectedHabitGroups)
}
/**
@@ -91,11 +103,38 @@ class HabitCardListAdapter @Inject constructor(
}
override fun getItemCount(): Int {
return cache.habitCount
return cache.itemCount
}
override fun getItemId(position: Int): Long {
return getItem(position)!!.id!!
val uuidString = getItemUUID(position)
return if (uuidString != null) {
val formattedUUIDString = formatUUID(uuidString)
val uuid = UUID.fromString(formattedUUIDString)
uuid.mostSignificantBits and Long.MAX_VALUE
} else {
-1
}
}
fun getItemUUID(position: Int): String? {
val h = cache.getHabitByPosition(position)
val hgr = cache.getHabitGroupByPosition(position)
return if (h != null) {
h.uuid!!
} else if (hgr != null) {
hgr.uuid!!
} else {
null
}
}
private fun formatUUID(uuidString: String): String {
return uuidString.substring(0, 8) + "-" +
uuidString.substring(8, 12) + "-" +
uuidString.substring(12, 16) + "-" +
uuidString.substring(16, 20) + "-" +
uuidString.substring(20, 32)
}
/**
@@ -104,7 +143,7 @@ class HabitCardListAdapter @Inject constructor(
* @return true if selection is empty, false otherwise
*/
val isSelectionEmpty: Boolean
get() = selected.isEmpty()
get() = selectedHabits.isEmpty() && selectedHabitGroups.isEmpty()
val isSortable: Boolean
get() = cache.primaryOrder == HabitList.Order.BY_POSITION
@@ -122,11 +161,18 @@ class HabitCardListAdapter @Inject constructor(
) {
if (listView == null) return
val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!)
val notes = cache.getNotes(habit.id!!)
val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
if (habit != null) {
val score = cache.getScore(habit.uuid!!)
val checkmarks = cache.getCheckmarks(habit.uuid!!)
val notes = cache.getNotes(habit.uuid!!)
val selected = selectedHabits.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
} else {
val habitGroup = cache.getHabitGroupByPosition(position)
val score = cache.getScore(habitGroup!!.uuid!!)
val selected = selectedHabitGroups.contains(habitGroup)
listView!!.bindGroupCardView(holder, habitGroup, score, selected)
}
}
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
@@ -141,8 +187,22 @@ class HabitCardListAdapter @Inject constructor(
parent: ViewGroup,
viewType: Int
): HabitCardViewHolder {
val view = listView!!.createHabitCardView()
return HabitCardViewHolder(view)
if (viewType == 0) {
val view = listView!!.createHabitCardView()
return HabitCardViewHolder(view, null)
} else {
val view = listView!!.createHabitGroupCardView()
return HabitCardViewHolder(null, view)
}
}
// function to override getItemViewType and return the type of the view. The view can either be a HabitCardView or a HabitGroupCardView
override fun getItemViewType(position: Int): Int {
return if (position < cache.habitCount) {
0
} else {
1
}
}
/**
@@ -190,7 +250,11 @@ class HabitCardListAdapter @Inject constructor(
* @param selected list of habits to be removed
*/
override fun performRemove(selected: List<Habit>) {
for (habit in selected) cache.remove(habit.id!!)
for (habit in selected) cache.remove(habit.uuid!!)
}
override fun performRemoveHabitGroup(selected: List<HabitGroup>) {
for (hgr in selected) cache.remove(hgr.uuid!!)
}
/**
@@ -250,10 +314,17 @@ class HabitCardListAdapter @Inject constructor(
* @param position position of the item to be toggled
*/
fun toggleSelection(position: Int) {
val h = getItem(position) ?: return
val k = selected.indexOf(h)
if (k < 0) selected.add(h) else selected.remove(h)
notifyDataSetChanged()
val h = cache.getHabitByPosition(position)
val hgr = cache.getHabitGroupByPosition(position)
if (h != null) {
val k = selectedHabits.indexOf(h)
if (k < 0) selectedHabits.add(h) else selectedHabits.remove(h)
notifyDataSetChanged()
} else if (hgr != null) {
val k = selectedHabitGroups.indexOf(hgr)
if (k < 0) selectedHabitGroups.add(hgr) else selectedHabitGroups.remove(hgr)
notifyDataSetChanged()
}
}
init {

View File

@@ -36,6 +36,7 @@ import dagger.Lazy
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.BundleSavedState
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject
@@ -79,7 +80,11 @@ class HabitCardListView(
}
fun createHabitCardView(): HabitCardView {
return cardViewFactory.create()
return cardViewFactory.createHabitCard()
}
fun createHabitGroupCardView(): HabitGroupCardView {
return cardViewFactory.createHabitGroupCard()
}
fun bindCardView(
@@ -110,8 +115,28 @@ class HabitCardListView(
return cardView
}
fun bindGroupCardView(
holder: HabitCardViewHolder,
habitGroup: HabitGroup,
score: Double,
selected: Boolean
): View {
val cardView = holder.itemView as HabitGroupCardView
cardView.habitGroup = habitGroup
cardView.isSelected = selected
cardView.score = score
val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev ->
detector.onTouchEvent(ev)
true
}
return cardView
}
fun attachCardView(holder: HabitCardViewHolder) {
(holder.itemView as HabitCardView).dataOffset = dataOffset
(holder.itemView as? HabitCardView)?.dataOffset = dataOffset
attachedHolders.add(holder)
}

View File

@@ -38,6 +38,7 @@ import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@@ -55,7 +56,8 @@ class HabitCardViewFactory
private val numberPanelFactory: NumberPanelViewFactory,
private val behavior: ListHabitsBehavior
) {
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
fun createHabitCard() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
fun createHabitGroupCard() = HabitGroupCardView(context, behavior)
}
class HabitCardView(
@@ -285,6 +287,32 @@ class HabitCardView(
}
}
private fun copyAttributesFrom(hgr: HabitGroup) {
fun getActiveColor(habitGroup: HabitGroup): Int {
return when (habitGroup.isArchived) {
true -> sres.getColor(R.attr.contrast60)
false -> currentTheme().color(habitGroup.color).toInt()
}
}
val c = getActiveColor(hgr)
label.apply {
text = hgr.name
setTextColor(c)
}
scoreRing.apply {
setColor(c)
}
checkmarkPanel.apply {
color = c
visibility = View.GONE
}
numberPanel.apply {
color = c
visibility = View.GONE
}
}
private fun triggerRipple(x: Float, y: Float) {
val background = innerFrame.background
background.setHotspot(x, y)

View File

@@ -21,4 +21,4 @@ package org.isoron.uhabits.activities.habits.list.views
import androidx.recyclerview.widget.RecyclerView
class HabitCardViewHolder(itemView: HabitCardView) : RecyclerView.ViewHolder(itemView)
class HabitCardViewHolder(itemView1: HabitCardView?, itemView2: HabitGroupCardView?) : RecyclerView.ViewHolder(itemView1 ?: itemView2!!)

View File

@@ -23,15 +23,6 @@ import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres
import javax.inject.Inject
class HabitGroupCardViewFactory
@Inject constructor(
@ActivityContext val context: Context,
private val behavior: ListHabitsBehavior
) {
fun create() = HabitGroupCardView(context, behavior)
}
class HabitGroupCardView(
@ActivityContext context: Context,
@@ -56,6 +47,7 @@ class HabitGroupCardView(
scoreRing.setPrecision(1.0f / 16)
}
var addButtonView: AddButtonView
private var innerFrame: LinearLayout
private var label: TextView
private var scoreRing: RingView
@@ -83,6 +75,8 @@ class HabitGroupCardView(
}
}
addButtonView = AddButtonView(context)
innerFrame = LinearLayout(context).apply {
gravity = Gravity.CENTER_VERTICAL
orientation = LinearLayout.HORIZONTAL
@@ -91,6 +85,7 @@ class HabitGroupCardView(
addView(scoreRing)
addView(label)
addView(addButtonView)
setOnTouchListener { v, event ->
v.background.setHotspot(event.x, event.y)

View File

@@ -1,5 +0,0 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.recyclerview.widget.RecyclerView
class HabitGroupCardViewHolder(itemView: HabitGroupCardView) : RecyclerView.ViewHolder(itemView)

View File

@@ -24,6 +24,7 @@
<string translatable="false" name="fa_arrow_circle_down">&#xf0ab;</string>
<string translatable="false" name="fa_check">&#xf00c;</string>
<string translatable="false" name="fa_times">&#xf00d;</string>
<string translatable="false" name="fa_plus">&#xf067;</string>
<string translatable="false" name="fa_skipped">&#xf068;</string>
<string translatable="false" name="fa_bell_o">&#xf0f3;</string>
<string translatable="false" name="fa_calendar">&#xf073;</string>