From 5c9ad9286dca3dd7705f0be7e1b728453f97b111 Mon Sep 17 00:00:00 2001 From: Grishka Date: Tue, 14 Nov 2023 19:23:42 +0300 Subject: [PATCH] Thread fragment tweaks part 1 --- .../fragments/BaseStatusListFragment.java | 2 +- .../android/fragments/ThreadFragment.java | 2 +- .../ExtendedFooterStatusDisplayItem.java | 78 +++++++-- .../ui/views/WrappingLinearLayout.java | 128 ++++++++++++++ .../main/res/drawable/divider_inset_16dp.xml | 9 + .../layout/display_item_extended_footer.xml | 164 ++++++++++++------ mastodon/src/main/res/values/attrs.xml | 5 + 7 files changed, 321 insertions(+), 67 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/WrappingLinearLayout.java create mode 100644 mastodon/src/main/res/drawable/divider_inset_16dp.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 261f2c78..c1edff6f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -713,7 +713,7 @@ public abstract class BaseStatusListFragment exten // Do not draw dividers between hashtag and/or account rows if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder)) return false; - return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP; + return !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP; } return false; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java index 9247ecca..6d274d9a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java @@ -69,7 +69,7 @@ public class ThreadFragment extends StatusListFragment{ } } } - items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus())); + items.add(items.size()-1, new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus())); } return items; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java index ed4b4359..417ce148 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java @@ -2,10 +2,13 @@ package org.joinmastodon.android.ui.displayitems; import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.view.View; import android.view.ViewGroup; @@ -21,18 +24,22 @@ import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; +import java.time.LocalDate; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.Locale; import androidx.annotation.PluralsRes; +import androidx.annotation.StringRes; import me.grishka.appkit.Nav; public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ public final Status status; - private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); + private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); + private static final DateTimeFormatter DATE_FORMATTER=DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){ super(parentID, parentFragment); @@ -45,7 +52,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ } public static class Holder extends StatusDisplayItem.Holder{ - private final TextView time; + private final TextView time, date, app, dateAppSeparator; private final TextView favorites, reblogs, editHistory; public Holder(Context context, ViewGroup parent){ @@ -53,30 +60,46 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ reblogs=findViewById(R.id.reblogs); favorites=findViewById(R.id.favorites); editHistory=findViewById(R.id.edit_history); - time=findViewById(R.id.timestamp); + time=findViewById(R.id.time); + date=findViewById(R.id.date); + app=findViewById(R.id.app_name); + dateAppSeparator=findViewById(R.id.date_app_separator); reblogs.setOnClickListener(v->startAccountListFragment(StatusReblogsListFragment.class)); favorites.setOnClickListener(v->startAccountListFragment(StatusFavoritesListFragment.class)); editHistory.setOnClickListener(v->startEditHistoryFragment()); + time.setOnClickListener(v->showTimeSnackbar()); + app.setOnClickListener(v->UiUtils.launchWebBrowser(context, item.status.application.website)); } @SuppressLint("DefaultLocale") @Override public void onBind(ExtendedFooterStatusDisplayItem item){ Status s=item.status; - favorites.setText(itemView.getResources().getQuantityString(R.plurals.x_favorites, (int)item.status.favouritesCount, item.status.favouritesCount)); - reblogs.setText(itemView.getResources().getQuantityString(R.plurals.x_reblogs, (int)item.status.reblogsCount, item.status.reblogsCount)); + favorites.setText(getFormattedPlural(R.plurals.x_favorites, item.status.favouritesCount)); + reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, item.status.reblogsCount)); if(s.editedAt!=null){ editHistory.setVisibility(View.VISIBLE); - editHistory.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt, false))); + ZonedDateTime dt=s.editedAt.atZone(ZoneId.systemDefault()); + String time=TIME_FORMATTER.format(dt); + if(!dt.toLocalDate().equals(LocalDate.now())){ + time+=" · "+DATE_FORMATTER.format(dt); + } + editHistory.setText(getFormattedSubstitutedString(R.string.last_edit_at_x, time)); }else{ editHistory.setVisibility(View.GONE); } - String timeStr=TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault())); + ZonedDateTime dt=item.status.createdAt.atZone(ZoneId.systemDefault()); + time.setText(TIME_FORMATTER.format(dt)); + date.setText(DATE_FORMATTER.format(dt)); if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){ - time.setText(item.parentFragment.getString(R.string.timestamp_via_app, timeStr, item.status.application.name)); + app.setVisibility(View.VISIBLE); + dateAppSeparator.setVisibility(View.VISIBLE); + app.setText(item.status.application.name); + app.setEnabled(!TextUtils.isEmpty(item.status.application.website)); }else{ - time.setText(timeStr); + app.setVisibility(View.GONE); + dateAppSeparator.setVisibility(View.GONE); } } @@ -85,14 +108,39 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ return false; } - private SpannableStringBuilder getFormattedPlural(@PluralsRes int res, int quantity){ - String str=item.parentFragment.getResources().getQuantityString(res, quantity, quantity); + private SpannableStringBuilder getFormattedPlural(@PluralsRes int res, long quantity){ + String str=item.parentFragment.getResources().getQuantityString(res, (int)quantity, quantity); String formattedNumber=String.format(Locale.getDefault(), "%,d", quantity); int index=str.indexOf(formattedNumber); SpannableStringBuilder ssb=new SpannableStringBuilder(str); if(index>=0){ - ssb.setSpan(new TypefaceSpan("sans-serif-medium"), index, index+formattedNumber.length(), 0); - ssb.setSpan(new ForegroundColorSpan(UiUtils.getThemeColor(item.parentFragment.getActivity(), android.R.attr.textColorPrimary)), index, index+formattedNumber.length(), 0); + ForegroundColorSpan colorSpan=new ForegroundColorSpan(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSurfaceVariant)); + ssb.setSpan(colorSpan, index, index+formattedNumber.length(), 0); + Object typefaceSpan; + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){ + typefaceSpan=new TypefaceSpan(Typeface.create(Typeface.DEFAULT, 600, false)); + }else{ + typefaceSpan=new StyleSpan(Typeface.BOLD); + } + ssb.setSpan(typefaceSpan, index, index+formattedNumber.length(), 0); + } + return ssb; + } + + private SpannableStringBuilder getFormattedSubstitutedString(@StringRes int res, String substitution){ + String str=item.parentFragment.getString(res, substitution); + int index=item.parentFragment.getString(res).indexOf("%s"); + SpannableStringBuilder ssb=new SpannableStringBuilder(str); + if(index>=0){ + ForegroundColorSpan colorSpan=new ForegroundColorSpan(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSurfaceVariant)); + ssb.setSpan(colorSpan, index, index+substitution.length(), 0); + Object typefaceSpan; + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){ + typefaceSpan=new TypefaceSpan(Typeface.create(Typeface.DEFAULT, 600, false)); + }else{ + typefaceSpan=new StyleSpan(Typeface.BOLD); + } + ssb.setSpan(typefaceSpan, index, index+substitution.length(), 0); } return ssb; } @@ -110,5 +158,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ args.putString("id", item.status.id); Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args); } + + private void showTimeSnackbar(){ + + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/WrappingLinearLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/WrappingLinearLayout.java new file mode 100644 index 00000000..4ec5088f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/WrappingLinearLayout.java @@ -0,0 +1,128 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import org.joinmastodon.android.R; + +import java.util.ArrayList; + +/** + * Something like a horizontal LinearLayout, but wraps child views onto a new line if they don't fit + */ +public class WrappingLinearLayout extends ViewGroup{ + private int verticalGap, horizontalGap; + private ArrayList rowHeights=new ArrayList<>(); + + public WrappingLinearLayout(Context context){ + this(context, null); + } + + public WrappingLinearLayout(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public WrappingLinearLayout(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.WrappingLinearLayout); + verticalGap=ta.getDimensionPixelOffset(R.styleable.WrappingLinearLayout_android_verticalGap, 0); + horizontalGap=ta.getDimensionPixelOffset(R.styleable.WrappingLinearLayout_android_horizontalGap, 0); + ta.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + int w=MeasureSpec.getSize(widthMeasureSpec)-getPaddingLeft()-getPaddingRight(); + int heightUsed=0, widthRemain=w, currentRowHeight=0; + rowHeights.clear(); + for(int i=0;iwidthRemain){ + // Doesn't fit into the current row. Start a new one. + heightUsed+=currentRowHeight+verticalGap; + rowHeights.add(currentRowHeight); + currentRowHeight=child.getMeasuredHeight()+verticalMargins; + widthRemain=w; + }else{ + // Does fit. Advance horizontally. + if(widthRemain=endPadding){ + xOffset+=childW+horizontalGap; + if(child.getLayoutParams() instanceof MarginLayoutParams mlp){ + xOffset+=mlp.leftMargin+mlp.rightMargin; + } + firstInRow=false; + }else{ + xOffset=rtl ? getPaddingRight() : getPaddingLeft(); + yOffset+=rowHeight+verticalGap; + currentRowIndex++; + childX=xOffset; + rowHeight=rowHeights.get(currentRowIndex); + } + if(childH + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_extended_footer.xml b/mastodon/src/main/res/layout/display_item_extended_footer.xml index b376012d..84b78768 100644 --- a/mastodon/src/main/res/layout/display_item_extended_footer.xml +++ b/mastodon/src/main/res/layout/display_item_extended_footer.xml @@ -4,73 +4,133 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" - android:divider="@drawable/divider_inset_16dp_start" + android:paddingBottom="8dp" + android:divider="@drawable/divider_inset_16dp" android:showDividers="middle"> - + android:paddingHorizontal="16dp" + android:paddingVertical="8dp" + android:horizontalGap="8dp" + android:verticalGap="8dp" + android:clipToPadding="false"> + + + + + + + + + + + + - + android:layout_height="wrap_content" + android:paddingHorizontal="16dp" + android:paddingVertical="8dp" + android:horizontalGap="8dp" + android:verticalGap="8dp" + android:clipToPadding="false"> - + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml index 69cb57f8..74b74508 100644 --- a/mastodon/src/main/res/values/attrs.xml +++ b/mastodon/src/main/res/values/attrs.xml @@ -47,4 +47,9 @@ + + + + + \ No newline at end of file