Adds a widget that works look like the homepage but will work on a android widget.

pull/2181/head
skippyy22 2 months ago
parent a9acbd6cab
commit 98ec166666

@ -0,0 +1,71 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class HabitListWidgetTest : BaseViewTest() {
private lateinit var listOfHabits: ArrayList<Habit>
private lateinit var view: FrameLayout
override fun setUp() {
super.setUp()
setTheme(R.style.WidgetTheme)
prefs.widgetOpacity = 255
listOfHabits.add(fixtures.createLongNumericalHabit().apply {
color = PaletteColor(11)
frequency = Frequency.WEEKLY
recompute()
})
listOfHabits.add(fixtures.createLongHabit().apply {
color = PaletteColor(1)
frequency = Frequency.DAILY
recompute()
})
val widget = HabitListWidget(targetContext, 0, listOfHabits)
view = convertToView(widget, 400, 400)
}
@Test
fun testIsInstalled() {
assertWidgetProviderIsInstalled(HabitListWidgetProvider::class.java)
}
@Test
@Throws(Exception::class)
fun testRender() {
assertRenders(view, PATH + "render.png")
}
companion object {
private const val PATH = "widgets/HabitListWidget/"
}
}

@ -112,6 +112,15 @@
</intent-filter>
</activity>
<activity
android:name=".widgets.activities.HabitPickerDialogMultiple"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".activities.about.AboutActivity"
android:label="@string/about">
@ -158,6 +167,18 @@
android:resource="@xml/widget_history_info" />
</receiver>
<receiver
android:name=".widgets.HabitListWidgetProvider"
android:exported="true"
android:label="@string/main_activity_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_habits_list_info" />
</receiver>
<receiver
android:name=".widgets.ScoreWidgetProvider"
android:exported="true"

@ -0,0 +1,620 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Typeface
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.view.View
import org.isoron.uhabits.R
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.StyledResources
import kotlin.math.max
import androidx.core.graphics.withTranslation
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.ui.screens.habits.show.views.IndividualHabitListState
import org.isoron.uhabits.utils.dim
import kotlin.math.min
import org.isoron.platform.gui.toInt
private val BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD)
private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL)
class HabitListChart : View {
private var habits = emptyList<IndividualHabitListState>()
private var weekDayStrings = emptyList<String>()
private var dateStrings = emptyList<String>()
private var amtHabits = 0
private var maxCheckMarks = 0
private var numCheckMarks = 0
private var habitRowSize = 0
private var checkMarkSize = 18.dpToPx()
private var textBoxSize = checkMarkSize * 2
private var padding = dpToPixels(context, 4f)
private var scaleFactor = 25f
private val rect = RectF()
private val barRect = RectF()
private var backGroundPaint: Paint? = null
private var lowContrastTextColor = 0 // contrast20
private var mediumContrastTextColor = 0 // contrast40
private var highContrastTextColor = 0 // contrast60
private val app = context.applicationContext as HabitsApplication
private val preferences = app.component.preferences
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
textSize = 12.spToPx()
color = Color.WHITE
}
private val pText: TextPaint = TextPaint().apply {
textSize = (dim(R.dimen.regularTextSize) * 1.5).toFloat()
typeface = BOLD_TYPEFACE
textAlign = Paint.Align.CENTER
isAntiAlias = true
}
private val pNumber: TextPaint = TextPaint().apply {
textSize = dim(R.dimen.smallTextSize)
typeface = BOLD_TYPEFACE
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val pUnit: TextPaint = TextPaint().apply {
textSize = getDimension(context, R.dimen.smallerTextSize)
typeface = NORMAL_TYPEFACE
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
// width
var widthSpec = widthSpec
val width = MeasureSpec.getSize(widthSpec)
// responsive height
var heightSpec = heightSpec
habitRowSize = resources.getDimensionPixelSize(R.dimen.baseSize ) + 60
val epsilonOfRowSize = 10
val height = MeasureSpec.getSize(heightSpec)
amtHabits = min(habits.size, (height / habitRowSize) - 1)
var habitHeight = habitRowSize * (amtHabits + 1)
if (amtHabits < 1){ // Always have at least one habit. Edge Case
amtHabits++
habitRowSize = height / (amtHabits + 1)
habitHeight = height
}
else if (habits.size != amtHabits && height - habitHeight >= habitRowSize * 0.50 && (height / (amtHabits + 1) >= habitRowSize - epsilonOfRowSize) ){ // If enough room at the bottom to fit another one, fit another one.
amtHabits++
habitRowSize = height / (amtHabits + 1)
habitHeight = height
}
else if (height - habitHeight < habitRowSize * 0.50 && (height / (amtHabits + 1) <= habitRowSize + epsilonOfRowSize) ){ // If not enough room to fit another one, make them all slightly bigger
habitRowSize = height / (amtHabits + 1)
habitHeight = height
}
heightSpec = MeasureSpec.makeMeasureSpec(habitHeight, MeasureSpec.EXACTLY)
widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
setMeasuredDimension(widthSpec, heightSpec)
// Amount of Checkmarks and the size of the habit text
val firstCheckMarkWithPadding = checkMarkSize + (padding * 4)
val otherCheckMarkWithPadding = checkMarkSize + (padding * 8)
fun getCheckMarkWithPadding(i: Int) = firstCheckMarkWithPadding + i * otherCheckMarkWithPadding
val ringSizeWithPadding = checkMarkSize + (padding * 1.5)
val rowPadding = padding * 2
val minTextBoxSize = checkMarkSize * 2 + padding
if (width < 150.dpToPx()) {
Log.e("JMO", "1")
numCheckMarks = 1
textBoxSize = (width - (rowPadding) - ( firstCheckMarkWithPadding ) - padding - (ringSizeWithPadding)).toFloat()
}
else {
Log.e("JMO", "2")
numCheckMarks = ((((width + (padding * 4) - (rowPadding)) * 0.62) / otherCheckMarkWithPadding)).toInt()
textBoxSize = (width - (rowPadding) - ( getCheckMarkWithPadding(numCheckMarks - 1) )- padding - (ringSizeWithPadding)).toFloat()
if (textBoxSize - otherCheckMarkWithPadding >= minTextBoxSize){
numCheckMarks++
textBoxSize -= otherCheckMarkWithPadding
}
}
if (numCheckMarks > maxCheckMarks)
numCheckMarks = maxCheckMarks
}
private fun init() {
backGroundPaint = Paint()
backGroundPaint!!.textAlign = Paint.Align.CENTER
backGroundPaint!!.isAntiAlias = true
val res = StyledResources(context)
lowContrastTextColor = res.getColor(R.attr.contrast20)
mediumContrastTextColor = res.getColor(R.attr.contrast40)
highContrastTextColor = res.getColor(R.attr.contrast60)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (habits.isEmpty()) return
val marginTop = (height - (habitRowSize * (amtHabits + 1))) / 2.0f
rect[0f, marginTop, width.toFloat()] = marginTop + habitRowSize
// Draw Dates Header
drawHeaderRow(canvas, rect)
rect.offset(0f, habitRowSize.toFloat())
// Draw the habit rows
for (i in 0 until minOf(habits.size, amtHabits)) {
drawRow(canvas, habits[i], rect)
rect.offset(0f, habitRowSize.toFloat())
}
}
private fun drawHeaderRow(canvas: Canvas, rect: RectF) {
val round = dpToPixels(context, 2f)
// Draw background box
backGroundPaint!!.color = Color.TRANSPARENT
barRect[rect.left + padding, rect.top + habitRowSize * 0.05f, rect.right - padding] =
rect.bottom - habitRowSize * 0.05f
canvas.drawRoundRect(barRect, round, round, backGroundPaint!!)
// paint
val paint = TextPaint().apply {
color = Color.WHITE
isAntiAlias = true
textSize = dim(R.dimen.tinyTextSize)
textAlign = Paint.Align.CENTER
typeface = Typeface.DEFAULT_BOLD
}
val em = paint.measureText("m")
val checkMarkCenterX = ((padding * 4) + checkMarkSize/2)
val checkMarkCenterY = rect.centerY()
// Draw dates
repeat(numCheckMarks) { index ->
val centerX = rect.right - (checkMarkCenterX * (index + 1) + (padding * 4) * (index))
val y1 = checkMarkCenterY - 0.25 * em
val y2 = checkMarkCenterY + 1.25 * em
var dateIndex = numCheckMarks - index - 1
if (preferences.isCheckmarkSequenceReversed){
dateIndex = index
}
canvas.drawText(weekDayStrings[dateIndex], centerX, y1.toFloat(), paint)
canvas.drawText(dateStrings[dateIndex], centerX, y2.toFloat(), paint)
}
}
private fun drawRow(canvas: Canvas, habit: IndividualHabitListState, rect: RectF) {
val round = dpToPixels(context, 2f)
// Draw background box
backGroundPaint!!.color = lowContrastTextColor
barRect[rect.left + padding, rect.top + habitRowSize * 0.05f, rect.right - padding] =
rect.bottom - habitRowSize * 0.05f
canvas.drawRoundRect(barRect, round, round, backGroundPaint!!)
// ScoreRing
val ringSize = checkMarkSize.toInt()
val ringCenterX = (rect.left + (padding * 1.5) + ringSize/2).toFloat()
val ringCenterY = rect.centerY()
drawRingView(
canvas = canvas,
centerX = ringCenterX,
centerY = ringCenterY,
percentage = habit.score,
color = habit.color.toInt()
)
// CheckMarks
val checkMarkCenterX = ((padding * 4) + checkMarkSize/2)
val checkMarkCenterY = rect.centerY()
for (index in 1..numCheckMarks) { // 1 , 2, 3 if numCheckMarks == 3
var habitCheckIndex = numCheckMarks - index
if (preferences.isCheckmarkSequenceReversed){
habitCheckIndex = index - 1
}
// Checkbox Rectangle
val centerX = rect.right - ((checkMarkCenterX * index) + (padding * 4) * (index - 1))
val centerY = checkMarkCenterY
val checkRect = RectF()
checkRect.set(
centerX - checkMarkSize / 2f,
centerY - checkMarkSize / 2f,
centerX + checkMarkSize / 2f,
centerY + checkMarkSize / 2f
)
val paint = Paint().apply {
this.color = habit.color.toInt()
style = Paint.Style.STROKE
strokeWidth = 1.dpToPx()
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
isAntiAlias = true
}
if (habit.isNumerical) { // If Numerical Habit
drawNumberCheck(
canvas = canvas,
checkRect = checkRect,
value = (max(-1, (habit.values[habitCheckIndex])) / 1000.0),
threshold = habit.targetValue,
units = habit.unit,
targetType = habit.targetType,
paint = paint
)
}
else { // If Non Numerical Habit
drawNonNumeric(
canvas = canvas,
checkRect = checkRect,
value = (max(-1, habit.values[habitCheckIndex])),
paint = paint
)
}
}
// Draw habit name
drawAdaptiveText(
canvas = canvas,
text = habit.name,
x = ringCenterX + ringSize/2 + padding,
y = rect.top,
width= textBoxSize.toInt(), // textAreaWidth,
height = habitRowSize,
paint = textPaint,
maxLines = 2
)
}
private fun drawRingView(
canvas: Canvas,
centerX: Float,
centerY: Float,
percentage: Float,
color: Int
) {
val thickness = checkMarkSize * 0.22f
val bgPaint = Paint().apply {
this.color = Color.argb(25, Color.red(color), Color.green(color), Color.blue(color))
style = Paint.Style.STROKE
strokeWidth = thickness
isAntiAlias = true
}
val fgPaint = Paint().apply {
this.color = color
style = Paint.Style.STROKE
strokeWidth = thickness
strokeCap = Paint.Cap.BUTT
isAntiAlias = true
}
val rect = RectF(
centerX - checkMarkSize / 2 + thickness / 2,
centerY - checkMarkSize / 2 + thickness / 2,
centerX - checkMarkSize / 2 + checkMarkSize - thickness / 2,
centerY - checkMarkSize / 2 + checkMarkSize - thickness / 2
)
// Draw background
canvas.drawArc(rect, 0f, 360f, false, bgPaint)
// Draw progress
if (percentage > 0) {
canvas.drawArc(rect, -90f, 360f * percentage, false, fgPaint)
}
}
private fun drawAdaptiveText(
canvas: Canvas,
text: String,
x: Float,
y: Float,
width: Int,
height: Int,
paint: TextPaint,
maxLines: Int = 2
) {
// Create text layout
val layout = StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0f, 1f)
.setEllipsize(TextUtils.TruncateAt.END)
.setMaxLines(maxLines)
.build()
// if single line, top-aligned if multi-line
val verticalPos = if (layout.lineCount == 1) {
y + (height - layout.height) / 2
} else {
y + (height - layout.height) / 2
}
canvas.withTranslation(x, verticalPos) {
layout.draw(this)
}
}
private fun drawNumberCheck(
canvas: Canvas,
checkRect: RectF,
value : Double,
threshold: Double,
units: String,
targetType : NumericalHabitType,
paint : Paint
) {
// Color
val activeColor = when {
value == SKIP.toDouble() / 1000 -> paint.color
value < 0.0 -> mediumContrastTextColor
(targetType == AT_LEAST) && (value >= threshold) -> paint.color
(targetType == AT_MOST) && (value <= threshold) -> paint.color
else -> highContrastTextColor
}
paint.color = activeColor
// Prepare text
val numberText = if (value >= 0) value.toShortString() else "0"
pNumber.color = activeColor
pUnit.color = activeColor
// Draw
val em = pNumber.measureText("m")
var questionMarkScale = 1.0
val verticalSpacing = em * 0.3f
if (units.isNotBlank()){ // if have units
questionMarkScale = 0.8
checkRect.offset(0f, - verticalSpacing)
}
// Draw Number
when {
value == SKIP.toDouble() / 1000 -> {
drawSkipLine(canvas, checkRect, paint)
}
value >= 0 -> {
canvas.drawText(numberText,checkRect.centerX(),checkRect.centerY() + em / 3, pNumber)
}
preferences.areQuestionMarksEnabled -> {
drawSimpleText(canvas, checkRect, pNumber, "?", questionMarkScale)
}
else -> {
canvas.drawText(numberText,checkRect.centerX(),checkRect.centerY() + em / 3, pNumber)
}
}
// Draw Units
if (units.isNotBlank()) { // if have units
val unitsSub = units.substring(0, min(units.length, 7))
checkRect.offset(0f, +verticalSpacing + em)
canvas.drawText(unitsSub, checkRect.centerX(), checkRect.centerY() + em / 3, pUnit)
}
}
private fun drawNonNumeric(
canvas: Canvas,
checkRect : RectF,
value: Int,
paint: Paint)
{
// Color
paint.color = when (value) {
YES_MANUAL, YES_AUTO, SKIP -> paint.color
NO -> {
if (preferences.areQuestionMarksEnabled) {
highContrastTextColor
} else {
mediumContrastTextColor
}
}
else -> mediumContrastTextColor
}
// Which CheckMark
when (value) {
SKIP -> drawSkipLine(canvas, checkRect, paint)
NO -> drawXMark(canvas, checkRect, paint)
UNKNOWN -> {
if (preferences.areQuestionMarksEnabled) {
drawSimpleText(canvas, checkRect, paint, "?")
} else {
drawXMark(canvas, checkRect, paint)
}
}
YES_AUTO -> {
drawCheckMark(canvas, checkRect, paint, false)
drawCheckMark(canvas, checkRect, paint, true)
}
else -> drawCheckMark(canvas, checkRect, paint)
}
}
private fun drawCheckMark(
canvas: Canvas,
checkRect: RectF,
paint: Paint,
isAuto: Boolean = false
) {
val scale = checkMarkSize / scaleFactor
canvas.withTranslation(checkRect.left , checkRect.top) {
scale(scale, scale)
// Draw Checkmark
val path = Path().apply {
moveTo(9f, 16.17f)
lineTo(4.83f, 12f)
lineTo(3.41f, 13.41f)
lineTo(9f, 19f)
lineTo(21f, 7f)
lineTo(19.59f, 5.59f)
close()
}
if (isAuto) {
// First draw: outline
paint.style = Paint.Style.STROKE
paint.strokeWidth = paint.strokeWidth // scale the stroke width
drawPath(path, paint)
// Second draw: inner fill
paint.style = Paint.Style.FILL
paint.color = mediumContrastTextColor // your background color
drawPath(path, paint)
} else {
// Regular checkmark
paint.style = Paint.Style.STROKE
drawPath(path, paint)
}
}
}
private fun drawSimpleText(
canvas: Canvas,
checkRect: RectF,
paint: Paint,
text: String,
scale: Double = 1.0
) {
pText.textSize = (dim(R.dimen.regularTextSize) * 1.5 * scale).toFloat()
pText.color = paint.color
val em = pText.measureText("m")
canvas.drawText(
text,
checkRect.centerX(),
checkRect.centerY() + em / 2,
pText
)
}
private fun drawSkipLine(
canvas: Canvas,
checkRect: RectF,
paint: Paint,
) {
val scale = checkMarkSize / scaleFactor
canvas.withTranslation(checkRect.left, checkRect.top) {
canvas.scale(scale, scale)
val lineWidth = 12f
val startX = 12f - lineWidth / 2
val endX = 12f + lineWidth / 2
val y = 12f
drawLine(startX, y, endX, y, paint)
}
}
private fun drawXMark(
canvas: Canvas,
checkRect: RectF,
paint: Paint,
) {
val scale = checkMarkSize / scaleFactor
canvas.withTranslation(checkRect.left, checkRect.top) {
canvas.scale(scale, scale)
// Create the X path
val path = Path().apply {
moveTo(6f, 6f)
lineTo(18f, 18f)
moveTo(6f, 18f)
lineTo(18f, 6f)
}
canvas.drawPath(path, paint)
}
}
fun setHabits(habits: List<IndividualHabitListState>) {
this.habits = habits
requestLayout()
}
fun setHeaderDates(weekDayStrings : List<String>, dateStrings : List<String>){
this.weekDayStrings = weekDayStrings
this.dateStrings = dateStrings
requestLayout()
}
fun setMaxCheckMarks(maxCheckMarks : Int){
this.maxCheckMarks = maxCheckMarks
}
private fun Int.dpToPx(): Float = this * resources.displayMetrics.density
private fun Int.spToPx(): Float = this * resources.displayMetrics.scaledDensity
}

@ -207,4 +207,15 @@ class PendingIntentFactory
data = Uri.parse(habit.uriString)
putExtra("timestamp", timestamp.unixTime)
}
fun showListHabitsActivity(): PendingIntent {
return getActivity(
context,
0,
Intent(context, ListHabitsActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
},
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
}
}

@ -0,0 +1,75 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import android.view.View
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.TextView
import org.isoron.uhabits.activities.common.views.HabitListChart
import org.isoron.uhabits.core.ui.screens.habits.show.views.HabitListCardPresenter
import org.isoron.uhabits.core.ui.views.WidgetTheme
import org.isoron.uhabits.widgets.views.GraphWidgetView
class HabitListWidget(
context: Context,
val widgetId: Int,
private val habits: List<Habit>,
stacked: Boolean = false
): BaseWidget(context, widgetId, stacked) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context): PendingIntent =
pendingIntentFactory.showListHabitsActivity()
override fun refreshData(view: View) {
val maxDays = 10
val data = HabitListCardPresenter.buildState(
habits = habits,
theme = WidgetTheme(),
maxDays = maxDays
)
val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
(widgetView.dataView as HabitListChart).apply{
setHabits(data.habits)
setHeaderDates(data.weekDayStrings, data.dateStrings)
setMaxCheckMarks(maxDays)
}
}
override fun buildView() =
GraphWidgetView(context, HabitListChart(context)).apply {
setTitle("Jordan Test")
val title = findViewById<View>(R.id.title) as TextView
title.textSize = 0.toFloat()
layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
}

@ -0,0 +1,50 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets
import android.content.Context
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitNotFoundException
import java.util.ArrayList
class HabitListWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getNullableHabitsFromWidgetId(context, id)
return HabitListWidget(context = context, widgetId = id, habits = habits)
}
private fun getNullableHabitsFromWidgetId(context: Context, widgetId: Int): List<Habit> {
val app = context.applicationContext as HabitsApplication
val widgetPrefs = app.component.widgetPreferences
val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId)
val habits = app.component.habitList
val selectedHabits = ArrayList<Habit>(selectedIds.size)
for (id in selectedIds) {
val h = habits.getById(id) ?: continue
selectedHabits.add(h)
}
if (selectedHabits.isEmpty()) {
throw HabitNotFoundException()
}
return selectedHabits
}
}

@ -84,6 +84,7 @@ class WidgetUpdater
updateWidgets(modifiedHabitId, StreakWidgetProvider::class.java)
updateWidgets(modifiedHabitId, FrequencyWidgetProvider::class.java)
updateWidgets(modifiedHabitId, TargetWidgetProvider::class.java)
updateWidgets(modifiedHabitId, HabitListWidgetProvider::class.java)
}
}

@ -0,0 +1,94 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.activities
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
import android.os.Bundle
import android.widget.AbsListView.CHOICE_MODE_MULTIPLE
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ListView
import android.widget.TextView
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.widgets.WidgetUpdater
class HabitPickerDialogMultiple : HabitPickerDialog() {
private var widgetId = 0
private lateinit var widgetPreferences: WidgetPreferences
private lateinit var widgetUpdater: WidgetUpdater
override fun shouldHideNumerical() = false
override fun shouldHideBoolean() = false
override fun getEmptyMessage() = R.string.no_habits
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val component = (applicationContext as HabitsApplication).component
AndroidThemeSwitcher(this, component.preferences).apply()
val habitList = component.habitList
widgetPreferences = component.widgetPreferences
widgetUpdater = component.widgetUpdater
widgetId = intent.extras?.getInt(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID) ?: 0
val habitIds = ArrayList<Long>()
val habitNames = ArrayList<String>()
for (h in habitList) {
if (h.isArchived) continue
if (h.isNumerical and shouldHideNumerical()) continue
if (!h.isNumerical and shouldHideBoolean()) continue
habitIds.add(h.id!!)
habitNames.add(h.name)
}
if (habitNames.isEmpty()) {
setContentView(R.layout.widget_empty_activity)
findViewById<TextView>(R.id.message).setText(getEmptyMessage())
return
}
setContentView(R.layout.widget_configure_activity_multiple)
val listView = findViewById<ListView>(R.id.listView)
val saveButton = findViewById<Button>(R.id.buttonSave)
with(listView) {
adapter = ArrayAdapter(
context,
android.R.layout.simple_list_item_multiple_choice,
habitNames
)
choiceMode = CHOICE_MODE_MULTIPLE
itemsCanFocus = false
}
saveButton.setOnClickListener {
val selectedIds = mutableListOf<Long>()
for (i in 0..listView.count) {
if (listView.isItemChecked(i)) {
selectedIds.add(habitIds[i])
}
}
confirm(selectedIds)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1">
</ListView>
<Button android:id="@+id/buttonSave" android:layout_height="wrap_content" android:layout_width="match_parent" android:text="@string/save"/>
</LinearLayout>

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="50dp"
android:minWidth="100dp"
android:initialLayout="@layout/widget_graph"
android:previewImage="@drawable/widget_preview_habit_list"
android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.activities.HabitPickerDialogMultiple"
android:widgetCategory="home_screen">
</appwidget-provider>

@ -0,0 +1,118 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.platform.gui.Color
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils
import java.util.ArrayList
import java.util.GregorianCalendar
data class HabitListState(
val habits: List<IndividualHabitListState> = listOf(),
val weekDayStrings: List<String>,
val dateStrings: List<String>
)
data class IndividualHabitListState(
val color: Color,
val score: Float,
val isNumerical: Boolean,
val values: List<Int> = listOf(),
val targetValue: Double,
val targetType: NumericalHabitType,
val name: String,
val unit: String,
)
class HabitListCardPresenter {
companion object {
fun buildState(
habits: List<Habit>,
theme: Theme,
maxDays: Int = 5
): HabitListState {
val habitStateLists = ArrayList<IndividualHabitListState>()
val weekDayStrings = ArrayList<String>()
val dateStrings = ArrayList<String>()
for (habit in habits){
habitStateLists.add(IndividualHabitListCardPresenter.buildState(habit, theme, maxDays))
}
// Get Days
val day = DateUtils.getStartOfTodayCalendarWithOffset()
day.add(GregorianCalendar.DAY_OF_MONTH, 0)
repeat(maxDays) { index ->
val lines = DateUtils.formatHeaderDate(day).uppercase().split("\n")
weekDayStrings.add(lines[0])
dateStrings.add(lines[1])
day.add(GregorianCalendar.DAY_OF_MONTH, -1)
}
return HabitListState(
habitStateLists,
weekDayStrings = weekDayStrings,
dateStrings = dateStrings
)
}
}
}
class IndividualHabitListCardPresenter {
companion object {
fun buildState(
habit: Habit,
theme: Theme,
maxDays: Int
): IndividualHabitListState {
val state = OverviewCardPresenter.buildState(
habit = habit,
theme = theme
)
val color = theme.color(habit.color)
val score = state.scoreMonthDiff
val isNumerical = habit.isNumerical
val today = DateUtils.getTodayWithOffset()
val values = ArrayList<Int>()
for (index in 0..maxDays) {
values.add(habit.computedEntries.get(today.minus(index)).value)
}
val targetValue =habit.targetValue
val targetType = habit.targetType
val name = habit.name
val unit = habit.unit
return IndividualHabitListState(
color = color,
score = score,
isNumerical = isNumerical,
values = values,
targetValue = targetValue,
targetType = targetType,
name = name,
unit = unit,
)
}
}
}
Loading…
Cancel
Save