Link card improvements (AND-115)

closes #651
This commit is contained in:
Grishka 2023-11-29 07:51:11 +03:00
parent a2ea8e76fb
commit a200701e4c
12 changed files with 217 additions and 45 deletions

View File

@ -11,6 +11,7 @@ import org.joinmastodon.android.ui.utils.BlurHashDecoder;
import org.joinmastodon.android.ui.utils.BlurHashDrawable; import org.joinmastodon.android.ui.utils.BlurHashDrawable;
import org.parceler.Parcel; import org.parceler.Parcel;
import java.time.Instant;
import java.util.List; import java.util.List;
@Parcel @Parcel
@ -34,11 +35,14 @@ public class Card extends BaseModel{
public String embedUrl; public String embedUrl;
public String blurhash; public String blurhash;
public List<History> history; public List<History> history;
public Instant publishedAt;
public transient Drawable blurhashPlaceholder; public transient Drawable blurhashPlaceholder;
@Override @Override
public void postprocess() throws ObjectValidationException{ public void postprocess() throws ObjectValidationException{
if(type==null)
type=Type.LINK;
super.postprocess(); super.postprocess();
if(blurhash!=null){ if(blurhash!=null){
Bitmap placeholder=BlurHashDecoder.decode(blurhash, 16, 16); Bitmap placeholder=BlurHashDecoder.decode(blurhash, 16, 16);
@ -64,6 +68,7 @@ public class Card extends BaseModel{
", embedUrl='"+embedUrl+'\''+ ", embedUrl='"+embedUrl+'\''+
", blurhash='"+blurhash+'\''+ ", blurhash='"+blurhash+'\''+
", history="+history+ ", history="+history+
", publishedAt="+publishedAt+
'}'; '}';
} }

View File

@ -1,6 +1,8 @@
package org.joinmastodon.android.ui.displayitems; package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
@ -13,6 +15,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Card; import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@ -35,7 +38,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public Type getType(){ public Type getType(){
return Type.CARD; return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height) ? Type.CARD_LARGE : Type.CARD_COMPACT;
} }
@Override @Override
@ -49,36 +52,65 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
} }
public static class Holder extends StatusDisplayItem.Holder<LinkCardStatusDisplayItem> implements ImageLoaderViewHolder{ public static class Holder extends StatusDisplayItem.Holder<LinkCardStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView title, description, domain; private final TextView title, description, domain, timestamp;
private final ImageView photo; private final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear; private boolean didClear;
private final View inner;
private final boolean isLarge;
public Holder(Context context, ViewGroup parent){ public Holder(Context context, ViewGroup parent, boolean isLarge){
super(context, R.layout.display_item_link_card, parent); super(context, isLarge ? R.layout.display_item_link_card : R.layout.display_item_link_card_compact, parent);
this.isLarge=isLarge;
title=findViewById(R.id.title); title=findViewById(R.id.title);
description=findViewById(R.id.description); description=findViewById(R.id.description);
domain=findViewById(R.id.domain); domain=findViewById(R.id.domain);
timestamp=findViewById(R.id.timestamp);
photo=findViewById(R.id.photo); photo=findViewById(R.id.photo);
findViewById(R.id.inner).setOnClickListener(this::onClick); inner=findViewById(R.id.inner);
inner.setOnClickListener(this::onClick);
inner.setOutlineProvider(OutlineProviders.roundedRect(12));
inner.setClipToOutline(true);
} }
@SuppressLint("SetTextI18n")
@Override @Override
public void onBind(LinkCardStatusDisplayItem item){ public void onBind(LinkCardStatusDisplayItem item){
Card card=item.status.card; Card card=item.status.card;
title.setText(card.title); title.setText(card.title);
description.setText(card.description); if(description!=null){
description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE); description.setText(card.description);
domain.setText(Uri.parse(card.url).getHost()); description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE);
}
String cardDomain=Uri.parse(card.url).getHost();
if(isLarge && !TextUtils.isEmpty(card.authorName)){
domain.setText(itemView.getContext().getString(R.string.article_by_author, card.authorName)+" · "+cardDomain);
}else{
domain.setText(cardDomain);
}
if(card.publishedAt!=null){
timestamp.setVisibility(View.VISIBLE);
timestamp.setText(" · "+UiUtils.formatRelativeTimestamp(itemView.getContext(), card.publishedAt));
}else{
timestamp.setVisibility(View.GONE);
}
photo.setImageDrawable(null); photo.setImageDrawable(null);
if(item.imgRequest!=null){ if(item.imgRequest!=null){
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
photo.setBackground(null);
photo.setImageTintList(null);
crossfadeDrawable.setSize(card.width, card.height); crossfadeDrawable.setSize(card.width, card.height);
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder); crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f); crossfadeDrawable.setCrossfadeAlpha(0f);
photo.setImageDrawable(null); photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable); photo.setImageDrawable(crossfadeDrawable);
didClear=false; didClear=false;
}else{
photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant));
photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline)));
photo.setScaleType(ImageView.ScaleType.CENTER);
photo.setImageResource(R.drawable.ic_feed_48px);
} }
} }

View File

@ -68,7 +68,8 @@ public abstract class StatusDisplayItem{
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent); case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent); case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent); case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true);
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false);
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent); case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null)); case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent); case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
@ -208,7 +209,8 @@ public abstract class StatusDisplayItem{
AUDIO, AUDIO,
POLL_OPTION, POLL_OPTION,
POLL_FOOTER, POLL_FOOTER,
CARD, CARD_LARGE,
CARD_COMPACT,
FOOTER, FOOTER,
ACCOUNT, ACCOUNT,
HASHTAG, HASHTAG,

View File

@ -87,7 +87,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset; boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
StatusDisplayItem.Type type=sdi.getItem().getType(); StatusDisplayItem.Type type=sdi.getItem().getType();
if(type==StatusDisplayItem.Type.CARD || type==StatusDisplayItem.Type.MEDIA_GRID) if(type==StatusDisplayItem.Type.CARD_LARGE || type==StatusDisplayItem.Type.MEDIA_GRID)
outRect.left=outRect.right=V.dp(16); outRect.left=outRect.right=V.dp(16);
else else
outRect.left=outRect.right=V.dp(8); outRect.left=outRect.right=V.dp(8);

View File

@ -1,11 +1,14 @@
package org.joinmastodon.android.ui.views; package org.joinmastodon.android.ui.views;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.ImageView; import android.widget.ImageView;
import org.joinmastodon.android.R;
public class FixedAspectRatioImageView extends ImageView{ public class FixedAspectRatioImageView extends ImageView{
private float aspectRatio=1; private float aspectRatio;
private boolean useHeight; private boolean useHeight;
public FixedAspectRatioImageView(Context context){ public FixedAspectRatioImageView(Context context){
@ -18,6 +21,10 @@ public class FixedAspectRatioImageView extends ImageView{
public FixedAspectRatioImageView(Context context, AttributeSet attrs, int defStyle){ public FixedAspectRatioImageView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle); super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FixedAspectRatioImageView);
aspectRatio=ta.getFloat(R.styleable.FixedAspectRatioImageView_aspectRatio, 1);
useHeight=ta.getBoolean(R.styleable.FixedAspectRatioImageView_useHeight, false);
ta.recycle();
} }
@Override @Override

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_on_surface_overlay">
<item android:id="@android:id/mask">
<shape>
<corners android:radius="11dp"/>
<solid android:color="#000"/>
</shape>
</item>
<item>
<shape>
<corners android:radius="11dp"/>
<stroke android:color="?colorM3OutlineVariant" android:width="1dp"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@android:color/white"
android:pathData="M9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H32.1L42,15.9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V17.55H30.45V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM13.95,33.45H34.05V30.45H13.95ZM13.95,17.55H24V14.55H13.95ZM13.95,25.5H34.05V22.5H13.95ZM9,9V17.55V9V17.55V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/>
</vector>

View File

@ -3,54 +3,75 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingBottom="8dp">
<org.joinmastodon.android.ui.views.MaxWidthFrameLayout <LinearLayout
android:id="@+id/inner" android:id="@+id/inner"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="250dp" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:foreground="?android:selectableItemBackground" android:orientation="vertical"
android:foreground="@drawable/fg_link_card"
android:padding="1dp"
android:maxWidth="400dp"> android:maxWidth="400dp">
<ImageView <org.joinmastodon.android.ui.views.FixedAspectRatioImageView
android:id="@+id/photo" android:id="@+id/photo"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:importantForAccessibility="no"
app:aspectRatio="1.7777777778"
tools:src="#0f0"/> tools:src="#0f0"/>
<LinearLayout <TextView
android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_marginHorizontal="16dp"
android:padding="8dp" android:layout_marginTop="16dp"
android:orientation="vertical" android:textAppearance="@style/m3_title_medium"
android:background="@drawable/window_bg_alpha95"> android:maxLines="3"
<TextView android:ellipsize="end"
android:id="@+id/title" android:textColor="?colorM3OnSurface"
android:layout_width="match_parent" tools:text="Link title"/>
android:layout_height="wrap_content" <TextView
android:textAppearance="@style/m3_body_large" android:id="@+id/description"
android:singleLine="true" android:layout_width="match_parent"
android:ellipsize="end" android:layout_height="wrap_content"
android:textColor="?colorM3OnSurface" android:layout_marginHorizontal="16dp"
tools:text="Link title"/> android:maxLines="2"
<TextView android:ellipsize="end"
android:id="@+id/description" android:textAppearance="@style/m3_body_medium"
android:layout_width="match_parent" android:textColor="?colorM3OnSurface"
android:layout_height="wrap_content" tools:text="Link description"/>
android:maxLines="2" <org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:ellipsize="end" android:layout_width="match_parent"
android:textAppearance="@style/m3_body_medium" android:layout_height="20dp"
android:textColor="?colorM3OnSurface" android:layout_marginHorizontal="16dp"
tools:text="Link description"/> android:layout_marginTop="8dp"
android:layout_marginBottom="16dp">
<TextView <TextView
android:id="@+id/domain" android:id="@+id/domain"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium" android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant" android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/> tools:text="example.com"/>
</LinearLayout> <TextView
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout> android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/>
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
</LinearLayout>
</FrameLayout> </FrameLayout>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingBottom="8dp">
<RelativeLayout
android:id="@+id/inner"
android:layout_width="match_parent"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
android:foreground="@drawable/fg_link_card"
android:padding="1dp"
android:maxWidth="400dp">
<ImageView
android:id="@+id/photo"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_alignParentEnd="true"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/photo"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_title_medium"
android:maxLines="3"
android:ellipsize="end"
android:textColor="?colorM3OnSurface"
tools:text="Link title"/>
<org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@id/photo"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/domain"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/>
<TextView
android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/>
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
</RelativeLayout>
</FrameLayout>

View File

@ -54,4 +54,9 @@
<attr name="android:verticalGap" format="dimension"/> <attr name="android:verticalGap" format="dimension"/>
<attr name="android:horizontalGap" format="dimension"/> <attr name="android:horizontalGap" format="dimension"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="FixedAspectRatioImageView">
<attr name="aspectRatio" format="float"/>
<attr name="useHeight" format="boolean"/>
</declare-styleable>
</resources> </resources>

View File

@ -665,4 +665,5 @@
<string name="app_version_copied">Version number copied to clipboard</string> <string name="app_version_copied">Version number copied to clipboard</string>
<string name="onboarding_recommendations_intro">You curate your own home feed.The more people you follow, the more active and interesting it will be.</string> <string name="onboarding_recommendations_intro">You curate your own home feed.The more people you follow, the more active and interesting it will be.</string>
<string name="onboarding_recommendations_title">Personalize your home feed</string> <string name="onboarding_recommendations_title">Personalize your home feed</string>
<string name="article_by_author">By %s</string>
</resources> </resources>

View File

@ -375,6 +375,7 @@
<style name="m3_body_medium"> <style name="m3_body_medium">
<item name="android:textSize">14dp</item> <item name="android:textSize">14dp</item>
<item name="android:lineSpacingExtra">4dp</item> <item name="android:lineSpacingExtra">4dp</item>
<item name="android:lineHeight">20dp</item>
</style> </style>
<style name="m3_body_small"> <style name="m3_body_small">
@ -386,6 +387,7 @@
<item name="android:fontFamily">sans-serif-medium</item> <item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">16dp</item> <item name="android:textSize">16dp</item>
<item name="android:lineSpacingExtra">5dp</item> <item name="android:lineSpacingExtra">5dp</item>
<item name="android:lineHeight">24dp</item>
</style> </style>
<style name="m3_title_small"> <style name="m3_title_small">