Thread fragment tweaks part 1

This commit is contained in:
Grishka 2023-11-14 19:23:42 +03:00
parent 688c0e2e85
commit 5c9ad9286d
7 changed files with 321 additions and 67 deletions

View File

@ -713,7 +713,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> 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;
}

View File

@ -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;
}

View File

@ -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<ExtendedFooterStatusDisplayItem>{
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(){
}
}
}

View File

@ -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<Integer> 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;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
LayoutParams lp=child.getLayoutParams();
int horizontalPadding=getPaddingLeft()+getPaddingRight();
int verticalPadding=getPaddingTop()+getPaddingBottom();
int horizontalMargins=0, verticalMargins=0;
if(lp instanceof MarginLayoutParams mlp){
horizontalPadding+=mlp.leftMargin+mlp.rightMargin;
verticalPadding+=mlp.topMargin+mlp.bottomMargin;
horizontalMargins=mlp.leftMargin+mlp.rightMargin;
verticalMargins=mlp.topMargin+mlp.bottomMargin;
}
child.measure(getChildMeasureSpec(widthMeasureSpec, horizontalPadding, lp.width), getChildMeasureSpec(heightMeasureSpec, verticalPadding, lp.height));
currentRowHeight=Math.max(child.getMeasuredHeight()+verticalMargins, currentRowHeight);
if(child.getMeasuredWidth()+(widthRemain<w ? horizontalGap : 0)+horizontalMargins>widthRemain){
// 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<w)
widthRemain-=horizontalGap;
widthRemain-=child.getMeasuredWidth()+horizontalMargins;
}
}
// Take last row into account
heightUsed+=currentRowHeight;
rowHeights.add(currentRowHeight);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), heightUsed+getPaddingTop()+getPaddingBottom());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
if(rowHeights.isEmpty())
return;
boolean rtl=getLayoutDirection()==LAYOUT_DIRECTION_RTL;
int xOffset=rtl ? getPaddingRight() : getPaddingLeft();
int endPadding=rtl ? getPaddingLeft() : getPaddingRight();
int yOffset=getPaddingTop();
int w=getWidth()-getPaddingLeft()-getPaddingRight();
int currentRowIndex=0;
boolean firstInRow=true;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
int childW=child.getMeasuredWidth();
int childH=child.getMeasuredHeight();
int rowHeight=rowHeights.get(currentRowIndex);
int childY, childX=xOffset;
if(getWidth()-(xOffset+childW+(firstInRow ? 0 : horizontalGap))>=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<rowHeight){
childY=yOffset+rowHeight/2-childH/2;
}else{
childY=yOffset;
}
if(rtl){
childX=getWidth()-childX-childW;
}
if(child.getLayoutParams() instanceof MarginLayoutParams mlp){
childX+=rtl ? mlp.rightMargin : mlp.leftMargin;
childY+=mlp.topMargin;
}
child.layout(childX, childY, childX+childW, childY+childH);
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs){
return new MarginLayoutParams(getContext(), attrs);
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="16dp" android:right="16dp">
<shape>
<solid android:color="?colorM3OutlineVariant"/>
<size android:height="0.5dp"/>
</shape>
</item>
</layer-list>

View File

@ -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">
<TextView
android:id="@+id/timestamp"
<org.joinmastodon.android.ui.views.WrappingLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:minHeight="20dp"
android:gravity="center_vertical"
android:textAppearance="@style/m3_body_large"
android:textSize="16sp"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="Dec 12, 2021, 12:42 PM via Mastodon for Android"/>
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:horizontalGap="8dp"
android:verticalGap="8dp"
android:clipToPadding="false">
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:background="@drawable/bg_button_borderless_rounded"
android:backgroundTint="?colorM3Secondary"
android:singleLine="true"
android:ellipsize="end"
android:paddingVertical="4dp"
android:paddingHorizontal="8dp"
android:layout_marginVertical="-4dp"
android:layout_marginHorizontal="-8dp"
tools:text="8:24 AM"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:importantForAccessibility="no"
android:text="·"/>
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:singleLine="true"
android:ellipsize="end"
tools:text="2023-11-08"/>
<TextView
android:id="@+id/date_app_separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:importantForAccessibility="no"
android:text="·"/>
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:singleLine="true"
android:ellipsize="end"
android:paddingVertical="4dp"
android:paddingHorizontal="8dp"
android:layout_marginVertical="-4dp"
android:layout_marginHorizontal="-8dp"
android:background="@drawable/bg_button_borderless_rounded"
android:backgroundTint="?colorM3Secondary"
tools:text="Mastodon for Android dfjklafjdsalkfjdslakfjdsaklfjdslak"/>
</org.joinmastodon.android.ui.views.WrappingLinearLayout>
<TextView
android:id="@+id/edit_history"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_height="36dp"
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:background="?android:selectableItemBackground"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:gravity="center_vertical"
android:drawableStart="@drawable/ic_edit_24px"
android:drawableTint="?colorM3OnSurfaceVariant"
android:drawablePadding="16dp"
tools:text="Last edit bla bla"/>
<org.joinmastodon.android.ui.views.WrappingLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:horizontalGap="8dp"
android:verticalGap="8dp"
android:clipToPadding="false">
<TextView
android:id="@+id/reblogs"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:background="?android:selectableItemBackground"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:background="@drawable/bg_button_borderless_rounded"
android:backgroundTint="?colorM3Secondary"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:drawableStart="@drawable/ic_repeat_24px"
android:drawableTint="?colorM3OnSurfaceVariant"
android:drawablePadding="16dp"
android:paddingVertical="4dp"
android:paddingHorizontal="8dp"
android:layout_marginVertical="-4dp"
android:layout_marginHorizontal="-8dp"
tools:text="123 boosts"/>
<TextView
android:id="@+id/favorites"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:background="?android:selectableItemBackground"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Secondary"
android:background="@drawable/bg_button_borderless_rounded"
android:backgroundTint="?colorM3Secondary"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:drawableStart="@drawable/ic_star_24px"
android:drawableTint="?colorM3OnSurfaceVariant"
android:drawablePadding="16dp"
android:paddingVertical="4dp"
android:paddingHorizontal="8dp"
android:layout_marginVertical="-4dp"
android:layout_marginHorizontal="-8dp"
tools:text="123 favorites"/>
</org.joinmastodon.android.ui.views.WrappingLinearLayout>
</LinearLayout>

View File

@ -47,4 +47,9 @@
<declare-styleable name="NestedRecyclerScrollView">
<attr name="takePriorityOverChildViews" format="boolean"/>
</declare-styleable>
<declare-styleable name="WrappingLinearLayout">
<attr name="android:verticalGap" format="dimension"/>
<attr name="android:horizontalGap" format="dimension"/>
</declare-styleable>
</resources>