Reporting M3 redesign

This commit is contained in:
Grishka
2023-05-22 17:08:04 +03:00
parent 34a2af8429
commit bd7157c172
75 changed files with 2371 additions and 488 deletions

View File

@@ -0,0 +1,51 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
import java.time.Instant;
import java.util.function.Predicate;
public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
public CheckableHeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
super(parentID, user, createdAt, parentFragment, accountID, status, extraText);
}
@Override
public Type getType(){
return Type.HEADER_CHECKABLE;
}
public static class Holder extends HeaderStatusDisplayItem.Holder{
private final View checkbox;
private final CheckableRelativeLayout view;
private Predicate<Holder> isChecked;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_header_checkable, parent);
checkbox=findViewById(R.id.checkbox);
view=(CheckableRelativeLayout) itemView;
checkbox.setBackground(new CheckBox(activity).getButtonDrawable());
}
@Override
public void onBind(HeaderStatusDisplayItem item){
super.onBind(item);
if(isChecked!=null){
view.setChecked(isChecked.test(this));
}
}
public void setIsChecked(Predicate<Holder> isChecked){
this.isChecked=isChecked;
}
}
}

View File

@@ -3,18 +3,16 @@ package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.graphics.Outline;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
@@ -43,6 +41,7 @@ import java.time.Instant;
import java.util.Collections;
import java.util.List;
import androidx.annotation.LayoutRes;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
@@ -114,7 +113,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private APIRequest<?> currentRelationshipRequest;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_header, parent);
this(activity, R.layout.display_item_header, parent);
}
protected Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
super(activity, layout, parent);
name=findViewById(R.id.name);
timeAndUsername=findViewById(R.id.time_and_username);
avatar=findViewById(R.id.avatar);
@@ -165,6 +168,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
args.putParcelable("relationship", Parcels.wrap(relationship));
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(activity, item.status.url);

View File

@@ -48,6 +48,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
public final Status status;
public boolean sensitiveRevealed;
public String sensitiveTitle;
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
super(parentID, parentFragment);
@@ -103,6 +104,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final LayerDrawable sensitiveOverlayBG;
private static final ColorDrawable drawableForWhenThereIsNoBlurhash=new ColorDrawable(0xffffffff);
private final TextView hideSensitiveButton;
private final TextView sensitiveText;
private int altTextIndex=-1;
private Animator altTextAnimator;
@@ -112,7 +114,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
wrapper=(FrameLayout)itemView;
layout=new MediaGridLayout(activity);
wrapper.addView(layout);
wrapper.setPadding(0, 0, 0, V.dp(8));
wrapper.setClipToPadding(false);
overlays=new MaxWidthFrameLayout(activity);
@@ -136,15 +137,20 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
activity.getLayoutInflater().inflate(R.layout.overlay_image_sensitive, overlays);
sensitiveOverlay=findViewById(R.id.sensitive_overlay);
sensitiveOverlayBG=(LayerDrawable) sensitiveOverlay.getBackground();
sensitiveOverlayBG=(LayerDrawable) sensitiveOverlay.getBackground().mutate();
sensitiveOverlayBG.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(false));
sensitiveOverlayBG.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(true));
sensitiveOverlay.setBackground(sensitiveOverlayBG);
sensitiveOverlay.setOnClickListener(v->revealSensitive());
hideSensitiveButton.setOnClickListener(v->hideSensitive());
sensitiveText=findViewById(R.id.sensitive_text);
}
@Override
public void onBind(MediaGridStatusDisplayItem item){
wrapper.setPadding(0, 0, 0, item.inset ? 0 : V.dp(8));
if(altTextAnimator!=null)
altTextAnimator.cancel();
@@ -190,6 +196,10 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
layout.setVisibility(View.VISIBLE);
}
hideSensitiveButton.setVisibility(item.status.sensitive ? View.VISIBLE : View.GONE);
if(!TextUtils.isEmpty(item.sensitiveTitle))
sensitiveText.setText(item.sensitiveTitle);
else
sensitiveText.setText(R.string.sensitive_content_explain);
}
@Override
@@ -346,5 +356,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
item.sensitiveRevealed=false;
V.setVisibilityAnimated(sensitiveOverlay, View.VISIBLE, ()->layout.setVisibility(View.INVISIBLE));
}
public MediaGridLayout getLayout(){
return layout;
}
public View getSensitiveOverlay(){
return sensitiveOverlay;
}
}
}

View File

@@ -33,6 +33,11 @@ public abstract class StatusDisplayItem{
public boolean inset;
public int index;
public static final int FLAG_INSET=1;
public static final int FLAG_NO_FOOTER=1 << 1;
public static final int FLAG_CHECKABLE=1 << 2;
public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3;
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
this.parentID=parentID;
this.parentFragment=parentFragment;
@@ -51,6 +56,7 @@ public abstract class StatusDisplayItem{
public static BindableViewHolder<? extends StatusDisplayItem> createViewHolder(Type type, Activity activity, ViewGroup parent){
return switch(type){
case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent);
case HEADER_CHECKABLE -> new CheckableHeaderStatusDisplayItem.Holder(activity, parent);
case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent);
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
@@ -70,6 +76,15 @@ public abstract class StatusDisplayItem{
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter){
int flags=0;
if(inset)
flags|=FLAG_INSET;
if(!addFooter)
flags|=FLAG_NO_FOOTER;
return buildItems(fragment, status, accountID, parentObject, knownAccounts, flags);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, int flags){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
Status statusForContent=status.getContentStatus();
@@ -80,7 +95,10 @@ public abstract class StatusDisplayItem{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_reply_20px));
}
HeaderStatusDisplayItem header;
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
if((flags & FLAG_CHECKABLE)!=0)
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
else
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
ArrayList<StatusDisplayItem> contentItems;
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
@@ -99,7 +117,10 @@ public abstract class StatusDisplayItem{
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
if(!imageAttachments.isEmpty()){
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
contentItems.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
contentItems.add(mediaGrid);
}
for(Attachment att:statusForContent.mediaAttachments){
if(att.type==Attachment.Type.AUDIO){
@@ -112,12 +133,13 @@ public abstract class StatusDisplayItem{
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && TextUtils.isEmpty(statusForContent.spoilerText)){
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
}
if(addFooter){
if((flags & FLAG_NO_FOOTER)==0){
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment));
}
int i=1;
boolean inset=(flags & FLAG_INSET)!=0;
for(StatusDisplayItem item:items){
item.inset=inset;
item.index=i++;
@@ -156,7 +178,8 @@ public abstract class StatusDisplayItem{
EXTENDED_FOOTER,
MEDIA_GRID,
SPOILER,
SECTION_HEADER
SECTION_HEADER,
HEADER_CHECKABLE
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{

View File

@@ -35,6 +35,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.PopupMenu;
@@ -740,4 +741,13 @@ public class UiUtils{
ta.recycle();
return d;
}
public static WindowInsets applyBottomInsetToFixedView(View view, WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
view.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(40)) : 0);
return insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
}
return insets;
}
}

View File

@@ -228,6 +228,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
args.putParcelable("relationship", Parcels.wrap(relationship));
Nav.go(fragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(fragment.getActivity(), account.url);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.LinearLayout;
@@ -47,4 +48,11 @@ public class CheckableLinearLayout extends LinearLayout implements Checkable{
}
return drawableState;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(info);
info.setCheckable(true);
info.setChecked(checked);
}
}

View File

@@ -0,0 +1,58 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.RelativeLayout;
public class CheckableRelativeLayout extends RelativeLayout implements Checkable{
private boolean checked;
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public CheckableRelativeLayout(Context context){
this(context, null);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
public void setChecked(boolean checked){
this.checked=checked;
refreshDrawableState();
}
@Override
public boolean isChecked(){
return checked;
}
@Override
public void toggle(){
setChecked(!checked);
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(info);
info.setCheckable(true);
info.setChecked(checked);
}
}

View File

@@ -0,0 +1,83 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.Switch;
import java.lang.reflect.Field;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.V;
public class M3Switch extends Switch{
private boolean ignoreRequestLayout;
private DummyDrawable dummyDrawable=new DummyDrawable();
public M3Switch(Context context){
super(context);
}
public M3Switch(Context context, AttributeSet attrs){
super(context, attrs);
}
public M3Switch(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
ignoreRequestLayout=true;
Drawable prevThumbDrawable=getThumbDrawable();
setThumbDrawable(dummyDrawable);
ignoreRequestLayout=false;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ignoreRequestLayout=true;
setThumbDrawable(prevThumbDrawable);
ignoreRequestLayout=false;
try{
Field fld=Switch.class.getDeclaredField("mThumbWidth");
fld.setAccessible(true);
fld.set(this, V.dp(32));
}catch(Exception ignore){}
}
@Override
public void requestLayout(){
if(ignoreRequestLayout)
return;
super.requestLayout();
}
private static class DummyDrawable extends Drawable{
@Override
public void draw(@NonNull Canvas canvas){
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return 0;
}
@Override
public int getIntrinsicWidth(){
return V.dp(26);
}
}
}