From 182df2bfae90403c84953e2f1922969fa2af68d9 Mon Sep 17 00:00:00 2001 From: UlrichKu Date: Tue, 21 Mar 2023 19:44:35 +0100 Subject: [PATCH] 3408 home help message (#3415) * 3408: First draft of help message on empty home timeline * 3408: Move image spanning to utils; tweak gui a bit (looks like status) * 3408: Use proper R again; appease linter * 3408: Add doc; remove narrow comment * 3408: null is default * 3408: Add German text * 3408: Stack refresh animation on top of help message (reorder) --- .../components/timeline/TimelineFragment.kt | 3 + .../com/keylesspalace/tusky/util/SpanUtils.kt | 93 ++++++++++++++----- .../tusky/view/BackgroundMessageView.kt | 16 +++- .../res/drawable/help_message_background.xml | 14 +++ .../res/layout-sw640dp/fragment_timeline.xml | 31 ++++--- app/src/main/res/layout/fragment_timeline.xml | 45 ++++----- .../res/layout/view_background_message.xml | 77 +++++++++------ app/src/main/res/values-de/strings.xml | 9 +- app/src/main/res/values/strings.xml | 5 + 9 files changed, 200 insertions(+), 93 deletions(-) create mode 100644 app/src/main/res/drawable/help_message_background.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index 3c288152c..d1e891d00 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -236,6 +236,9 @@ class TimelineFragment : if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { binding.statusView.show() binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) + if (kind == TimelineViewModel.Kind.HOME) { + binding.statusView.showHelp(R.string.help_empty_home) + } } } is LoadState.Error -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt index cba176131..0da64c1c2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt @@ -1,10 +1,19 @@ package com.keylesspalace.tusky.util +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Build import android.text.Spannable +import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.CharacterStyle +import android.text.style.DynamicDrawableSpan import android.text.style.ForegroundColorSpan +import android.text.style.ImageSpan import android.text.style.URLSpan +import androidx.appcompat.content.res.AppCompatResources +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import java.util.regex.Pattern import kotlin.math.max @@ -61,6 +70,66 @@ private class PatternFinder( val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE) } +/** + * Takes text containing mentions and hashtags and urls and makes them the given colour. + */ +fun highlightSpans(text: Spannable, colour: Int) { + // Strip all existing colour spans. + for (spanClass in spanClasses) { + clearSpans(text, spanClass) + } + + // Colour the mentions and hashtags. + val string = text.toString() + val length = text.length + var start = 0 + var end = 0 + while (end in 0 until length && start >= 0) { + // Search for url first because it can contain the other characters + val found = findPattern(string, end) + start = found.start + end = found.end + if (start in 0 until end) { + text.setSpan(getSpan(found.matchType, string, colour, start, end), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + start += finders[found.matchType]!!.searchPrefixWidth + } + } +} + +/** + * Replaces text of the form [drawabale name] or [iconics name] with their spanned counterparts (ImageSpan). + */ +fun addDrawables(text: CharSequence, color: Int, size: Int, context: Context): Spannable { + val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) DynamicDrawableSpan.ALIGN_CENTER else DynamicDrawableSpan.ALIGN_BASELINE + + val builder = SpannableStringBuilder(text) + + val pattern = Pattern.compile("\\[(drawable|iconics) ([0-9a-z_]+)\\]") + val matcher = pattern.matcher(builder) + while (matcher.find()) { + val resourceType = matcher.group(1) + val resourceName = matcher.group(2) + ?: continue + + val drawable: Drawable? = when (resourceType) { + "iconics" -> IconicsDrawable(context, GoogleMaterial.getIcon(resourceName)) + else -> { + val drawableResourceId = context.resources.getIdentifier(resourceName, "drawable", context.packageName) + if (drawableResourceId != 0) AppCompatResources.getDrawable(context, drawableResourceId) else null + } + } + + if (drawable != null) { + drawable.setBounds(0, 0, size, size) + drawable.setTint(color) + + builder.setSpan(ImageSpan(drawable, alignment), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + return builder +} + private fun clearSpans(text: Spannable, spanClass: Class) { for (span in text.getSpans(0, text.length, spanClass)) { text.removeSpan(span) @@ -136,30 +205,6 @@ private fun getSpan(matchType: FoundMatchType, string: String, colour: Int, star } } -/** Takes text containing mentions and hashtags and urls and makes them the given colour. */ -fun highlightSpans(text: Spannable, colour: Int) { - // Strip all existing colour spans. - for (spanClass in spanClasses) { - clearSpans(text, spanClass) - } - - // Colour the mentions and hashtags. - val string = text.toString() - val length = text.length - var start = 0 - var end = 0 - while (end in 0 until length && start >= 0) { - // Search for url first because it can contain the other characters - val found = findPattern(string, end) - start = found.start - end = found.end - if (start in 0 until end) { - text.setSpan(getSpan(found.matchType, string, colour, start, end), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - start += finders[found.matchType]!!.searchPrefixWidth - } - } -} - private fun isWordCharacters(codePoint: Int): Boolean { return (codePoint in 0x30..0x39) || // [0-9] (codePoint in 0x41..0x5a) || // [A-Z] diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt index 82860f9fc..4ccd5627e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -6,15 +6,16 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout +import android.widget.TextView import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding +import com.keylesspalace.tusky.util.addDrawables import com.keylesspalace.tusky.util.visible /** - * This view is used for screens with downloadable content which may fail. - * Can show an image, text and button below them. + * This view is used for screens with content which may be empty or might have failed to download. */ class BackgroundMessageView @JvmOverloads constructor( context: Context, @@ -47,4 +48,15 @@ class BackgroundMessageView @JvmOverloads constructor( binding.button.setOnClickListener(clickListener) binding.button.visible(clickListener != null) } + + fun showHelp(@StringRes helpRes: Int) { + val size: Int = binding.helpText.textSize.toInt() + 2 + val color = binding.helpText.currentTextColor + val text = context.getText(helpRes) + val textWithDrawables = addDrawables(text, color, size, context) + + binding.helpText.setText(textWithDrawables, TextView.BufferType.SPANNABLE) + + binding.helpText.visible(true) + } } diff --git a/app/src/main/res/drawable/help_message_background.xml b/app/src/main/res/drawable/help_message_background.xml new file mode 100644 index 000000000..cb28b7987 --- /dev/null +++ b/app/src/main/res/drawable/help_message_background.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout-sw640dp/fragment_timeline.xml b/app/src/main/res/layout-sw640dp/fragment_timeline.xml index e85798eb5..53e13a64b 100644 --- a/app/src/main/res/layout-sw640dp/fragment_timeline.xml +++ b/app/src/main/res/layout-sw640dp/fragment_timeline.xml @@ -12,18 +12,6 @@ android:layout_gravity="center_horizontal" android:background="?android:attr/colorBackground"> - - - - - - + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml index d3e716d6b..e24e4cd34 100644 --- a/app/src/main/res/layout/fragment_timeline.xml +++ b/app/src/main/res/layout/fragment_timeline.xml @@ -6,17 +6,18 @@ android:layout_height="match_parent" android:background="?android:attr/colorBackground"> - - - - - + android:layout_height="match_parent" + android:layout_marginBottom="?attr/actionBarSize" + android:visibility="gone" + app:layout_constrainedHeight="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> - + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/view_background_message.xml b/app/src/main/res/layout/view_background_message.xml index 2d07ff8db..65e9cc5ac 100644 --- a/app/src/main/res/layout/view_background_message.xml +++ b/app/src/main/res/layout/view_background_message.xml @@ -1,39 +1,60 @@ - + + + android:gravity="center" + android:orientation="vertical"> - + -