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 231356c24..70a55654c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -13,13 +13,11 @@ import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.animation.TranslateAnimation; import android.widget.ImageButton; -import android.widget.ImageButton; import android.widget.Toolbar; import org.joinmastodon.android.E; @@ -34,12 +32,10 @@ import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.BetterItemAnimator; -import org.joinmastodon.android.ui.PhotoLayoutHelper; -import org.joinmastodon.android.ui.TileGridLayoutManager; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; @@ -47,8 +43,10 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem; import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; +import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; import org.joinmastodon.android.ui.utils.UiUtils; -import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout; +import org.joinmastodon.android.ui.views.MediaGridLayout; +import org.joinmastodon.android.utils.TypedObjectPool; import java.util.ArrayList; import java.util.Collections; @@ -59,7 +57,6 @@ import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; @@ -83,6 +80,7 @@ public abstract class BaseStatusListFragment exten protected HashMap knownAccounts=new HashMap<>(); protected HashMap relationships=new HashMap<>(); protected Rect tmpRect=new Rect(); + protected TypedObjectPool attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView); public BaseStatusListFragment(){ super(20); @@ -192,21 +190,21 @@ public abstract class BaseStatusListFragment exten } @Override - public void openPhotoViewer(String parentID, Status _status, int attachmentIndex){ - final Status status=_status.reblog!=null ? _status.reblog : _status; + public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){ + final Status status=_status.getContentStatus(); currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){ - private ImageStatusDisplayItem.Holder transitioningHolder; + private MediaAttachmentViewController transitioningHolder; @Override public void setPhotoViewVisibility(int index, boolean visible){ - ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index); + MediaAttachmentViewController holder=findPhotoViewHolder(index); if(holder!=null) holder.photo.setAlpha(visible ? 1f : 0f); } @Override public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ - ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index); + MediaAttachmentViewController holder=findPhotoViewHolder(index); if(holder!=null){ transitioningHolder=holder; View view=transitioningHolder.photo; @@ -214,7 +212,8 @@ public abstract class BaseStatusListFragment exten view.getLocationOnScreen(pos); outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight()); list.setClipChildren(false); - transitioningHolder.itemView.setElevation(1f); + gridHolder.setClipChildren(false); + transitioningHolder.view.setElevation(1f); return true; } return false; @@ -241,15 +240,16 @@ public abstract class BaseStatusListFragment exten view.setTranslationY(0f); view.setScaleX(1f); view.setScaleY(1f); - transitioningHolder.itemView.setElevation(0f); + transitioningHolder.view.setElevation(0f); if(list!=null) list.setClipChildren(true); + gridHolder.setClipChildren(true); transitioningHolder=null; } @Override public Drawable getPhotoViewCurrentDrawable(int index){ - ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index); + MediaAttachmentViewController holder=findPhotoViewHolder(index); if(holder!=null) return holder.photo.getDrawable(); return null; @@ -265,23 +265,8 @@ public abstract class BaseStatusListFragment exten requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST); } - private ImageStatusDisplayItem.Holder findPhotoViewHolder(int index){ - if(list==null) - return null; - int offset=0; - for(StatusDisplayItem item:displayItems){ - if(item.parentID.equals(parentID)){ - if(item instanceof ImageStatusDisplayItem){ - RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(getMainAdapterOffset()+offset+index); - if(holder instanceof ImageStatusDisplayItem.Holder imgHolder){ - return imgHolder; - } - return null; - } - } - offset++; - } - return null; + private MediaAttachmentViewController findPhotoViewHolder(int index){ + return gridHolder.getViewController(index); } }); } @@ -368,31 +353,6 @@ public abstract class BaseStatusListFragment exten } } - @Override - protected RecyclerView.LayoutManager onCreateLayoutManager(){ - GridLayoutManager lm=new TileGridLayoutManager(getActivity(), 1000); - lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){ - @Override - public int getSpanSize(int position){ - position-=getMainAdapterOffset(); - if(position>=0 && position exten revealSpoiler(status, holder.getItemID()); } - public void onRevealSpoilerClick(ImageStatusDisplayItem.Holder holder){ + public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){ Status status=holder.getItem().status; revealSpoiler(status, holder.getItemID()); } @@ -559,13 +519,14 @@ public abstract class BaseStatusListFragment exten protected void updateImagesSpoilerState(Status status, String itemID){ ArrayList updatedPositions=new ArrayList<>(); - for(ImageStatusDisplayItem.Holder photo:(List)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){ - photo.setRevealed(status.spoilerRevealed); - updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset()); + MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class); + if(mediaGrid!=null){ + mediaGrid.setRevealed(status.spoilerRevealed); + updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset()); } int i=0; for(StatusDisplayItem item:displayItems){ - if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){ + if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){ adapter.notifyItemChanged(i); } i++; @@ -708,6 +669,15 @@ public abstract class BaseStatusListFragment exten return UiUtils.pickAccountForCompose(getActivity(), accountID); } + private MediaAttachmentViewController makeNewMediaAttachmentView(MediaGridStatusDisplayItem.GridItemType type){ + return new MediaAttachmentViewController(getActivity(), type); + } + + public TypedObjectPool getAttachmentViewsPool(){ + return attachmentViewsPool; + } + + protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ public DisplayItemsAdapter(){ @@ -745,16 +715,6 @@ public abstract class BaseStatusListFragment exten public ImageLoaderRequest getImageRequest(int position, int image){ return displayItems.get(position).getImageRequest(image); } - -// @Override -// public void onViewDetachedFromWindow(@NonNull BindableViewHolder holder){ -// if(holder instanceof ImageLoaderViewHolder){ -// int count=holder.getItem().getImageCount(); -// for(int i=0;i exten for(int i=0;i imgHolder){ + if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){ if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ hiddenMediaPaint.setColor(0x80000000); - PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; - float hGap=tile.startCol>0 ? V.dp(1) : 0; - float vGap=tile.startRow>0 ? V.dp(1) : 0; - c.drawRect(child.getX()-hGap, child.getY()-vGap, child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint); + c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint); } } } for(int i=0;i imgHolder){ + if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){ if(!imgHolder.getItem().status.spoilerRevealed){ - PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; - if(tile.startCol==0 && tile.startRow==0 && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ + if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ int listWidth=getListWidthForMediaLayout(); - int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH)); + int width=Math.min(listWidth, V.dp(MediaGridLayout.MAX_WIDTH)); if(currentMediaHiddenLayoutsWidth!=width) rebuildMediaHiddenLayouts(width-V.dp(32)); c.save(); @@ -831,47 +787,6 @@ public abstract class BaseStatusListFragment exten } } - @Override - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ - RecyclerView.ViewHolder holder=parent.getChildViewHolder(view); - if(holder instanceof ImageStatusDisplayItem.Holder){ - int listWidth=getListWidthForMediaLayout(); - int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH)); - PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem.Holder) holder).getItem().tiledLayout; - PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem.Holder) holder).getItem().thisTile; - if(tile.startCol+tile.colSpan1){ - outRect.bottom=-(Math.round(tile.height/1000f*width)-Math.round(layout.rowSizes[tile.startRow]/1000f*width)); - } - // ...and for its siblings, offset those on rows below first to the right where they belong - if(tile.startCol>0 && layout.tiles[0].rowSpan>1 && tile.startRow>layout.tiles[0].startRow){ - int xOffset=Math.round(layout.tiles[0].width/1000f*listWidth); - outRect.left=xOffset; - outRect.right=-xOffset; - } - - // If the width of the media block is smaller than that of the RecyclerView, offset the views horizontally to center them - if(listWidth>width){ - outRect.left+=(listWidth-V.dp(ImageAttachmentFrameLayout.MAX_WIDTH))/2; - if(tile.startCol>0){ - int spanOffset=0; - for(int i=0;i0 && !Character.isWhitespace(mainEditText.getText().charAt(start-1)) ? " :" : ":"; - mainEditText.getText().replace(start, mainEditText.getSelectionEnd(), prefix+emoji.shortcode+':'); + if(getActivity().getCurrentFocus() instanceof EditText edit){ + int start=edit.getSelectionStart(); + String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":"; + edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':'); + } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index dd31deeb9..a8e421746 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -19,9 +19,7 @@ import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; @@ -40,7 +38,6 @@ import java.util.stream.Stream; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; -import me.grishka.appkit.utils.V; public class NotificationsListFragment extends BaseStatusListFragment{ private boolean onlyMentions; @@ -98,13 +95,8 @@ public class NotificationsListFragment extends BaseStatusListFragment items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS, titleItem); - if(titleItem!=null){ - for(StatusDisplayItem item:items){ - if(item instanceof ImageStatusDisplayItem imgItem){ - imgItem.horizontalInset=V.dp(32); - } - } - } + if(titleItem!=null) + items.add(0, titleItem); return items; }else if(titleItem!=null){ AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java index f8c2fc050..c2a8dd2b3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java @@ -22,10 +22,8 @@ import org.joinmastodon.android.events.FinishReportFragmentsEvent; import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; @@ -132,22 +130,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ if(holder.getAbsoluteAdapterPosition()==0) return; outRect.left=V.dp(40); - if(holder instanceof ImageStatusDisplayItem.Holder imgHolder){ - PhotoLayoutHelper.TiledLayoutResult layout=imgHolder.getItem().tiledLayout; - PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; - String siblingID; - if(holder.getAbsoluteAdapterPosition()0) - outRect.left=0; - outRect.left+=V.dp(16); - outRect.right=V.dp(16); - if(!imgHolder.getItemID().equals(siblingID) || tile.startRow+tile.rowSpan==layout.rowSizes.length) - outRect.bottom=V.dp(16); - }else if(holder instanceof AudioStatusDisplayItem.Holder){ + if(holder instanceof AudioStatusDisplayItem.Holder){ outRect.bottom=V.dp(16); }else if(holder instanceof LinkCardStatusDisplayItem.Holder){ outRect.bottom=V.dp(16); @@ -166,10 +149,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ parent.getDecoratedBoundsWithMargins(child, tmpRect); String id=sdiHolder.getItemID(); int height=tmpRect.height(); - if(holder instanceof ImageStatusDisplayItem.Holder imgHolder){ - if(imgHolder.getItem().thisTile.startCol+imgHolder.getItem().thisTile.colSpan buildDisplayItems(Status s){ - List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null); - for(StatusDisplayItem item:items){ - if(item instanceof ImageStatusDisplayItem isdi){ - isdi.horizontalInset=V.dp(40+32); - } - } - return items; - } - protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){ parent.getDecoratedBoundsWithMargins(child, tmpRect); tmpRect.offset(0, Math.round(child.getTranslationY())); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java index 9f1a653a5..fe3accc4f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java @@ -11,8 +11,14 @@ import java.util.List; import androidx.annotation.NonNull; public class PhotoLayoutHelper{ + public static final int MAX_WIDTH=1000; + public static final int MAX_HEIGHT=1910; + @NonNull - public static TiledLayoutResult processThumbs(int _maxW, int _maxH, List thumbs){ + public static TiledLayoutResult processThumbs(List thumbs){ + int _maxW=MAX_WIDTH; + int _maxH=MAX_HEIGHT; + TiledLayoutResult result=new TiledLayoutResult(); if(thumbs.size()==1){ Attachment att=thumbs.get(0); @@ -45,13 +51,8 @@ public class PhotoLayoutHelper{ float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f; float maxW, maxH, marginW=0, marginH=0; - if(_maxW>0){ - maxW=_maxW; - maxH=_maxH; - }else{ - maxW=510; - maxH=510; - } + maxW=_maxW; + maxH=_maxH; float maxRatio=maxW/maxH; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GifVStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GifVStatusDisplayItem.java deleted file mode 100644 index 8461121b8..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GifVStatusDisplayItem.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.app.Activity; -import android.graphics.Outline; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; - -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; - -public class GifVStatusDisplayItem extends ImageStatusDisplayItem{ - public GifVStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile); - request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000); - } - - @Override - public Type getType(){ - return Type.GIFV; - } - - public static class Holder extends ImageStatusDisplayItem.Holder{ - - public Holder(Activity activity, ViewGroup parent){ - super(activity, R.layout.display_item_gifv, parent); - View play=findViewById(R.id.play_button); - play.setOutlineProvider(new ViewOutlineProvider(){ - @Override - public void getOutline(View view, Outline outline){ - outline.setOval(0, 0, view.getWidth(), view.getHeight()); - outline.setAlpha(.99f); // fixes shadow rendering - } - }); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java deleted file mode 100644 index b76cb997b..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java +++ /dev/null @@ -1,244 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import org.joinmastodon.android.GlobalUserPreferences; -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; -import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; -import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; -import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout; - -import androidx.annotation.LayoutRes; -import me.grishka.appkit.imageloader.ImageLoaderViewHolder; -import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; -import me.grishka.appkit.utils.CubicBezierInterpolator; - -public abstract class ImageStatusDisplayItem extends StatusDisplayItem{ - public final int index; - public final int totalPhotos; - protected Attachment attachment; - protected ImageLoaderRequest request; - public final Status status; - public final PhotoLayoutHelper.TiledLayoutResult tiledLayout; - public final PhotoLayoutHelper.TiledLayoutResult.Tile thisTile; - public int horizontalInset; - - public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment); - this.attachment=photo; - this.status=status; - this.index=index; - this.totalPhotos=totalPhotos; - this.tiledLayout=tiledLayout; - this.thisTile=thisTile; - } - - @Override - public int getImageCount(){ - return 1; - } - - @Override - public ImageLoaderRequest getImageRequest(int index){ - return request; - } - - public static abstract class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ - public final ImageView photo; - private ImageAttachmentFrameLayout layout; - private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); - private boolean didClear; - - private AnimatorSet currentAnim; - private final FrameLayout altTextWrapper; - private final TextView altTextButton; - private final ImageView noAltTextButton; - private final View altTextScroller; - private final ImageButton altTextClose; - private final TextView altText, noAltText; - - private View altOrNoAltButton; - private boolean altTextShown; - - public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){ - super(activity, layout, parent); - photo=findViewById(R.id.photo); - photo.setOnClickListener(this::onViewClick); - this.layout=(ImageAttachmentFrameLayout)itemView; - - altTextWrapper=findViewById(R.id.alt_text_wrapper); - altTextButton=findViewById(R.id.alt_button); - noAltTextButton=findViewById(R.id.no_alt_button); - altTextScroller=findViewById(R.id.alt_text_scroller); - altTextClose=findViewById(R.id.alt_text_close); - altText=findViewById(R.id.alt_text); - noAltText=findViewById(R.id.no_alt_text); - - altTextButton.setOnClickListener(this::onShowHideClick); - noAltTextButton.setOnClickListener(this::onShowHideClick); - altTextClose.setOnClickListener(this::onShowHideClick); -// altTextScroller.setNestedScrollingEnabled(true); - } - - @Override - public void onBind(ImageStatusDisplayItem item){ - layout.setLayout(item.tiledLayout, item.thisTile, item.horizontalInset); - crossfadeDrawable.setSize(item.attachment.getWidth(), item.attachment.getHeight()); - crossfadeDrawable.setBlurhashDrawable(item.attachment.blurhashPlaceholder); - crossfadeDrawable.setCrossfadeAlpha(item.status.spoilerRevealed ? 0f : 1f); - photo.setImageDrawable(null); - photo.setImageDrawable(crossfadeDrawable); - photo.setContentDescription(TextUtils.isEmpty(item.attachment.description) ? item.parentFragment.getString(R.string.media_no_description) : item.attachment.description); - didClear=false; - - if (currentAnim != null) currentAnim.cancel(); - - boolean altTextMissing = TextUtils.isEmpty(item.attachment.description); - altOrNoAltButton = altTextMissing ? noAltTextButton : altTextButton; - altTextShown=false; - - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - altTextButton.setVisibility(View.VISIBLE); - noAltTextButton.setVisibility(View.VISIBLE); - altTextButton.setAlpha(1f); - noAltTextButton.setAlpha(1f); - altTextWrapper.setVisibility(View.VISIBLE); - - if (altTextMissing){ - if (GlobalUserPreferences.showNoAltIndicator) { - noAltTextButton.setVisibility(View.VISIBLE); - noAltText.setVisibility(View.VISIBLE); - altTextWrapper.setBackgroundResource(R.drawable.bg_image_no_alt_overlay); - altTextButton.setVisibility(View.GONE); - altText.setVisibility(View.GONE); - } else { - altTextWrapper.setVisibility(View.GONE); - } - }else{ - if (GlobalUserPreferences.showAltIndicator) { - noAltTextButton.setVisibility(View.GONE); - noAltText.setVisibility(View.GONE); - altTextWrapper.setBackgroundResource(R.drawable.bg_image_alt_overlay); - altTextButton.setVisibility(View.VISIBLE); - altTextButton.setText(R.string.sk_alt_button); - altText.setVisibility(View.VISIBLE); - altText.setText(item.attachment.description); - altText.setPadding(0, 0, 0, 0); - } else { - altTextWrapper.setVisibility(View.GONE); - } - } - } - - private void onShowHideClick(View v){ - boolean show=v.getId()==R.id.alt_button || v.getId()==R.id.no_alt_button; - - if(altTextShown==show) - return; - if(currentAnim!=null) - currentAnim.cancel(); - - altTextShown=show; - if(show){ - altTextScroller.setVisibility(View.VISIBLE); - altTextClose.setVisibility(View.VISIBLE); - }else{ - altOrNoAltButton.setVisibility(View.VISIBLE); - // Hide these views temporarily so FrameLayout measures correctly - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - } - - // This is the current size... - int prevLeft=altTextWrapper.getLeft(); - int prevRight=altTextWrapper.getRight(); - int prevTop=altTextWrapper.getTop(); - altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ - @Override - public boolean onPreDraw(){ - altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this); - - // ...and this is after the layout pass, right now the FrameLayout has its final size, but we animate that change - if(!show){ - // Show these views again so they're visible for the duration of the animation. - // No one would notice they were missing during measure/layout. - altTextScroller.setVisibility(View.VISIBLE); - altTextClose.setVisibility(View.VISIBLE); - } - AnimatorSet set=new AnimatorSet(); - set.playTogether( - ObjectAnimator.ofInt(altTextWrapper, "left", prevLeft, altTextWrapper.getLeft()), - ObjectAnimator.ofInt(altTextWrapper, "right", prevRight, altTextWrapper.getRight()), - ObjectAnimator.ofInt(altTextWrapper, "top", prevTop, altTextWrapper.getTop()), - ObjectAnimator.ofFloat(altOrNoAltButton, View.ALPHA, show ? 1f : 0f, show ? 0f : 1f), - ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f), - ObjectAnimator.ofFloat(altTextClose, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f) - ); - set.setDuration(300); - set.setInterpolator(CubicBezierInterpolator.DEFAULT); - set.addListener(new AnimatorListenerAdapter(){ - @Override - public void onAnimationEnd(Animator animation){ - if(show){ - altOrNoAltButton.setVisibility(View.GONE); - }else{ - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - } - currentAnim=null; - } - }); - set.start(); - currentAnim=set; - - return true; - } - }); - } - - @Override - public void setImage(int index, Drawable drawable){ - crossfadeDrawable.setImageDrawable(drawable); - if(didClear && item.status.spoilerRevealed) - crossfadeDrawable.animateAlpha(0f); - } - - @Override - public void clearImage(int index){ - crossfadeDrawable.setCrossfadeAlpha(1f); - crossfadeDrawable.setImageDrawable(null); - didClear=true; - } - - private void onViewClick(View v){ - if(!item.status.spoilerRevealed){ - item.parentFragment.onRevealSpoilerClick(this); - }else if(item.parentFragment instanceof PhotoViewerHost){ - Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status; - ((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, contentStatus.mediaAttachments.indexOf(item.attachment)); - } - } - - public void setRevealed(boolean revealed){ - crossfadeDrawable.animateAlpha(revealed ? 0f : 1f); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java new file mode 100644 index 000000000..f2b3e3ab1 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java @@ -0,0 +1,311 @@ +package org.joinmastodon.android.ui.displayitems; + +import static org.joinmastodon.android.ui.utils.MediaAttachmentViewController.altWrapPadding; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.PhotoLayoutHelper; +import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; +import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; +import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild; +import org.joinmastodon.android.ui.views.MediaGridLayout; +import org.joinmastodon.android.utils.TypedObjectPool; + +import java.util.ArrayList; +import java.util.List; + +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.CubicBezierInterpolator; + +public class MediaGridStatusDisplayItem extends StatusDisplayItem{ + private static final String TAG="MediaGridDisplayItem"; + + private final PhotoLayoutHelper.TiledLayoutResult tiledLayout; + private final TypedObjectPool viewPool; + private final List attachments; + private final ArrayList requests=new ArrayList<>(); + public final Status status; + + public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List attachments, Status status){ + super(parentID, parentFragment); + this.tiledLayout=tiledLayout; + this.viewPool=parentFragment.getAttachmentViewsPool(); + this.attachments=attachments; + this.status=status; + for(Attachment att:attachments){ + requests.add(new UrlImageLoaderRequest(switch(att.type){ + case IMAGE -> att.url; + case VIDEO, GIFV -> att.previewUrl; + default -> throw new IllegalStateException("Unexpected value: "+att.type); + }, 1000, 1000)); + } + } + + @Override + public Type getType(){ + return Type.MEDIA_GRID; + } + + @Override + public int getImageCount(){ + return requests.size(); + } + + @Override + public ImageLoaderRequest getImageRequest(int index){ + return requests.get(index); + } + + public enum GridItemType{ + PHOTO, + VIDEO, + GIFV + } + + public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ + private final FrameLayout wrapper; + private final MediaGridLayout layout; + private final View.OnClickListener clickListener=this::onViewClick, altTextClickListener=this::onAltTextClick; + private final ArrayList controllers=new ArrayList<>(); + + private final FrameLayout altTextWrapper; + private final TextView altTextButton; + private final ImageView noAltTextButton; + private final View altTextScroller; + private final ImageButton altTextClose; + private final TextView altText, noAltText; + + private int altTextIndex=-1; + private Animator altTextAnimator; + + public Holder(Activity activity, ViewGroup parent){ + super(new FrameLayoutThatOnlyMeasuresFirstChild(activity)); + wrapper=(FrameLayout)itemView; + layout=new MediaGridLayout(activity); + wrapper.addView(layout); + + activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, wrapper); + altTextWrapper=findViewById(R.id.alt_text_wrapper); + altTextButton=findViewById(R.id.alt_button); + noAltTextButton=findViewById(R.id.no_alt_button); + altTextScroller=findViewById(R.id.alt_text_scroller); + altTextClose=findViewById(R.id.alt_text_close); + altText=findViewById(R.id.alt_text); + noAltText=findViewById(R.id.no_alt_text); + altTextClose.setOnClickListener(this::onAltTextCloseClick); + } + + @Override + public void onBind(MediaGridStatusDisplayItem item){ + if(altTextAnimator!=null) + altTextAnimator.cancel(); + + layout.setTiledLayout(item.tiledLayout); + for(MediaAttachmentViewController c:controllers){ + item.viewPool.reuse(c.type, c); + } + layout.removeAllViews(); + controllers.clear(); + int i=0; + for(Attachment att:item.attachments){ + MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){ + case IMAGE -> GridItemType.PHOTO; + case VIDEO -> GridItemType.VIDEO; + case GIFV -> GridItemType.GIFV; + default -> throw new IllegalStateException("Unexpected value: "+att.type); + }); + if(c.view.getLayoutParams()==null) + c.view.setLayoutParams(new MediaGridLayout.LayoutParams(item.tiledLayout.tiles[i])); + else + ((MediaGridLayout.LayoutParams) c.view.getLayoutParams()).tile=item.tiledLayout.tiles[i]; + layout.addView(c.view); + c.view.setOnClickListener(clickListener); + c.view.setTag(i); + if(c.btnsWrap!=null){ + c.btnsWrap.setOnClickListener(altTextClickListener); + c.btnsWrap.setTag(i); + c.btnsWrap.setAlpha(1f); + } + controllers.add(c); + c.bind(att, item.status); + i++; + } + altTextButton.setVisibility(View.VISIBLE); + noAltTextButton.setVisibility(View.VISIBLE); + altTextWrapper.setVisibility(View.GONE); + altTextIndex=-1; + } + + @Override + public void setImage(int index, Drawable drawable){ + controllers.get(index).setImage(drawable); + } + + @Override + public void clearImage(int index){ + controllers.get(index).clearImage(); + } + + private void onViewClick(View v){ + int index=(Integer)v.getTag(); + if(!item.status.spoilerRevealed){ + item.parentFragment.onRevealSpoilerClick(this); + }else if(item.parentFragment instanceof PhotoViewerHost){ + ((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, index, this); + } + } + + private void onAltTextClick(View v){ + if(altTextAnimator!=null) + altTextAnimator.cancel(); + v.setVisibility(View.INVISIBLE); + int index=(Integer)v.getTag(); + altTextIndex=index; + Attachment att=item.attachments.get(index); + boolean hasAltText = !TextUtils.isEmpty(att.description); + altTextButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); + noAltTextButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); + altText.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); + noAltText.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); + altText.setText(att.description); + altTextWrapper.setVisibility(View.VISIBLE); + altTextWrapper.setBackgroundResource(hasAltText ? R.drawable.bg_image_alt_overlay : R.drawable.bg_image_no_alt_overlay); + altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ + @Override + public boolean onPreDraw(){ + altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this); + + int[] loc={0, 0}; + v.getLocationInWindow(loc); + int btnL=loc[0], btnT=loc[1]; + wrapper.getLocationInWindow(loc); + btnL-=loc[0]; + btnT-=loc[1]; + + ArrayList anims=new ArrayList<>(); + anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0)); + anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1, 0)); + anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0, 1)); + anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0], altTextWrapper.getLeft())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1], altTextWrapper.getTop())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-altWrapPadding[2], altTextWrapper.getRight())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-altWrapPadding[3], altTextWrapper.getBottom())); + for(Animator a:anims) + a.setDuration(300); + + for(MediaAttachmentViewController c:controllers){ + if(c.btnsWrap!=null && c.btnsWrap!=v){ + anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1, 0).setDuration(150)); + } + } + + AnimatorSet set=new AnimatorSet(); + set.playTogether(anims); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + altTextAnimator=null; + for(MediaAttachmentViewController c:controllers){ + if(c.btnsWrap!=null){ + c.btnsWrap.setVisibility(View.INVISIBLE); + } + } + } + }); + altTextAnimator=set; + set.start(); + + return true; + } + }); + } + + private void onAltTextCloseClick(View v){ + if(altTextAnimator!=null) + altTextAnimator.cancel(); + + View btn=controllers.get(altTextIndex).btnsWrap; + for(MediaAttachmentViewController c:controllers){ + if(c.btnsWrap!=null && c.btnsWrap!=btn) { + c.btnsWrap.setVisibility(View.VISIBLE); + } + } + + int[] loc={0, 0}; + btn.getLocationInWindow(loc); + int btnL=loc[0], btnT=loc[1]; + wrapper.getLocationInWindow(loc); + btnL-=loc[0]; + btnT-=loc[1]; + + ArrayList anims=new ArrayList<>(); + anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1)); + anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1)); + anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0)); + anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0])); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1])); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-altWrapPadding[2])); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-altWrapPadding[3])); + for(Animator a:anims) + a.setDuration(300); + + for(MediaAttachmentViewController c:controllers){ +// if(c.btnsWrap!=null && c.btnsWrap!=btn){ + anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1).setDuration(150)); +// } + } + + AnimatorSet set=new AnimatorSet(); + set.playTogether(anims); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + altTextAnimator=null; + altTextWrapper.setVisibility(View.GONE); + btn.setVisibility(View.VISIBLE); + } + }); + altTextAnimator=set; + set.start(); + } + + public void setRevealed(boolean revealed){ + for(MediaAttachmentViewController c:controllers){ + c.setRevealed(revealed); + } + } + + public MediaAttachmentViewController getViewController(int index){ + return controllers.get(index); + } + + public void setClipChildren(boolean clip){ + layout.setClipChildren(clip); + wrapper.setClipChildren(clip); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java deleted file mode 100644 index a0609d10c..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ScrollView; -import android.widget.TextView; - -import org.joinmastodon.android.GlobalUserPreferences; -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; -import me.grishka.appkit.utils.CubicBezierInterpolator; -import me.grishka.appkit.utils.V; - -public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{ - public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment, photo, status, index, totalPhotos, tiledLayout, thisTile); - request=new UrlImageLoaderRequest(photo.url, 1000, 1000); - } - - @Override - public Type getType(){ - return Type.PHOTO; - } - - public static class Holder extends ImageStatusDisplayItem.Holder { - public Holder(Activity activity, ViewGroup parent) { - super(activity, R.layout.display_item_photo, parent); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 15b2c24a3..7b835fba8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -12,7 +12,6 @@ import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment; import org.joinmastodon.android.fragments.HomeTabFragment; -import org.joinmastodon.android.fragments.HomeTimelineFragment; import org.joinmastodon.android.fragments.ListTimelineFragment; import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ThreadFragment; @@ -20,7 +19,6 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.Filter; -import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.ScheduledStatus; @@ -31,11 +29,9 @@ import org.joinmastodon.android.utils.StatusFilterPredicate; import org.parceler.Parcels; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; import me.grishka.appkit.Nav; @@ -69,10 +65,7 @@ public abstract class StatusDisplayItem{ case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent); case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent); case TEXT -> new TextStatusDisplayItem.Holder(activity, parent); - case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent); - case GIFV -> new GifVStatusDisplayItem.Holder(activity, parent); case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent); - case VIDEO -> new VideoStatusDisplayItem.Holder(activity, parent); case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent); case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent); @@ -82,6 +75,7 @@ public abstract class StatusDisplayItem{ case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent); case GAP -> new GapStatusDisplayItem.Holder(activity, parent); case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent); + case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent); case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent); }; } @@ -159,20 +153,8 @@ public abstract class StatusDisplayItem{ header.needBottomPadding=true; List imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); if(!imageAttachments.isEmpty()){ - int photoIndex=0; - PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(1000, 1910, imageAttachments); - for(Attachment attachment:imageAttachments){ - if(attachment.type==Attachment.Type.IMAGE){ - items.add(new PhotoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex])); - }else if(attachment.type==Attachment.Type.GIFV){ - items.add(new GifVStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex])); - }else if(attachment.type==Attachment.Type.VIDEO){ - items.add(new VideoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex])); - }else{ - throw new IllegalStateException("This isn't supposed to happen, type is "+attachment.type); - } - photoIndex++; - } + PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments); + items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent)); } for(Attachment att:statusForContent.mediaAttachments){ if(att.type==Attachment.Type.AUDIO){ @@ -218,9 +200,6 @@ public abstract class StatusDisplayItem{ HEADER, REBLOG_OR_REPLY_LINE, TEXT, - PHOTO, - VIDEO, - GIFV, AUDIO, POLL_OPTION, POLL_FOOTER, @@ -230,8 +209,9 @@ public abstract class StatusDisplayItem{ ACCOUNT, HASHTAG, GAP, - WARNING, - EXTENDED_FOOTER + EXTENDED_FOOTER, + MEDIA_GRID, + WARNING } public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/VideoStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/VideoStatusDisplayItem.java deleted file mode 100644 index 41c54aeef..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/VideoStatusDisplayItem.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.app.Activity; -import android.graphics.Outline; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; - -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; - -public class VideoStatusDisplayItem extends ImageStatusDisplayItem{ - public VideoStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile); - request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000); - } - - @Override - public Type getType(){ - return Type.VIDEO; - } - - public static class Holder extends ImageStatusDisplayItem.Holder{ - - public Holder(Activity activity, ViewGroup parent){ - super(activity, R.layout.display_item_video, parent); - View play=findViewById(R.id.play_button); - play.setOutlineProvider(new ViewOutlineProvider(){ - @Override - public void getOutline(View view, Outline outline){ - outline.setOval(0, 0, view.getWidth(), view.getHeight()); - outline.setAlpha(.99f); // fixes shadow rendering - } - }); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java index 72dcb47ce..fc168087b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java @@ -1,7 +1,8 @@ package org.joinmastodon.android.ui.photoviewer; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; public interface PhotoViewerHost{ - void openPhotoViewer(String parentID, Status status, int attachmentIndex); + void openPhotoViewer(String parentID, Status status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 74189a796..414c78649 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -8,24 +8,25 @@ import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout; import android.text.Spanned; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.SoundEffectConstants; -import android.view.View; -import android.view.ViewConfiguration; import android.widget.TextView; +import androidx.annotation.NonNull; + +import org.joinmastodon.android.ui.utils.UiUtils; + import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { - private Paint hlPaint; + private final Paint hlPaint; private Path hlPath; private LinkSpan selectedSpan; - private TextView view; + private final TextView view; - private final Runnable longClickRunnable = () -> { - if (selectedSpan != null) selectedSpan.onLongClick(view); - }; + private final GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -33,11 +34,45 @@ public class ClickableLinksDelegate { hlPaint.setAntiAlias(true); hlPaint.setPathEffect(new CornerPathEffect(V.dp(3))); // view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light)); + gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler()); } public boolean onTouch(MotionEvent event) { - long eventDuration = event.getEventTime() - event.getDownTime(); - if(event.getAction()==MotionEvent.ACTION_DOWN){ + if(event.getAction()==MotionEvent.ACTION_CANCEL){ + // the gestureDetector does not provide a callback for CANCEL, therefore: + // remove background color of view before passing event to gestureDetector + resetAndInvalidate(); + } + return gestureDetector.onTouchEvent(event); + } + + /** + * remove highlighting from span and let the system redraw the view + */ + private void resetAndInvalidate() { + hlPath=null; + selectedSpan=null; + view.invalidate(); + } + + public void onDraw(Canvas canvas){ + if(hlPath!=null){ + canvas.save(); + canvas.translate(0, view.getPaddingTop()); + canvas.drawPath(hlPath, hlPaint); + canvas.restore(); + } + } + + /** + * GestureListener for spans that represent URLs. + * onDown: on start of touch event, set highlighting + * onSingleTapUp: when there was a (short) tap, call onClick and reset highlighting + * onLongPress: copy URL to clipboard, let user know, reset highlighting + */ + private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent event) { int line=-1; Rect rect=new Rect(); Layout l=view.getLayout(); @@ -52,8 +87,7 @@ public class ClickableLinksDelegate { return false; } CharSequence text=view.getText(); - if(text instanceof Spanned){ - Spanned s=(Spanned)text; + if(text instanceof Spanned s){ LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class); if(spans.length>0){ for(LinkSpan span:spans){ @@ -70,7 +104,6 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; - view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -96,35 +129,26 @@ public class ClickableLinksDelegate { } } } + return super.onDown(event); } - if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ - if (eventDuration <= ViewConfiguration.getLongPressTimeout()) { + + @Override + public boolean onSingleTapUp(@NonNull MotionEvent event) { + if(selectedSpan!=null){ view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); + resetAndInvalidate(); + return true; } - view.removeCallbacks(longClickRunnable); - hlPath=null; - selectedSpan=null; - view.invalidate(); return false; } - if(event.getAction()==MotionEvent.ACTION_CANCEL){ - hlPath=null; - selectedSpan=null; - view.removeCallbacks(longClickRunnable); - view.invalidate(); - return false; - } - return false; - } - - public void onDraw(Canvas canvas){ - if(hlPath!=null){ - canvas.save(); - canvas.translate(0, view.getPaddingTop()); - canvas.drawPath(hlPath, hlPaint); - canvas.restore(); - } - } + @Override + public void onLongPress(@NonNull MotionEvent event) { + if (selectedSpan == null) return; + UiUtils.copyText(view, selectedSpan.getType() == LinkSpan.Type.URL ? selectedSpan.getLink() : selectedSpan.getText()); + //reset view + resetAndInvalidate(); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java index e9c4fb0fc..2b80c076b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java @@ -46,14 +46,14 @@ public class LinkSpan extends CharacterStyle { } } - public void onLongClick(View view) { - UiUtils.copyText(view, getType() == Type.URL ? link : text); - } - public String getLink(){ return link; } + public String getText() { + return text; + } + public Type getType(){ return type; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java index a811fb464..37aca8964 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java @@ -8,10 +8,8 @@ import android.view.View; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.fragments.NotificationsListFragment; -import org.joinmastodon.android.ui.PhotoLayoutHelper; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import java.util.List; @@ -87,21 +85,11 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; boolean bottomSiblingInset=pos img){ - PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout; - PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile; - // only inset those items that are on the edges of the layout - insetLeft=tile.startCol==0; - insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length; - // inset all items in the bottom row - if(tile.startRow+tile.rowSpan==layout.rowSizes.length) - bottomSiblingInset=false; - } if(insetLeft) outRect.left=pad; if(insetRight) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java new file mode 100644 index 000000000..db0d9653f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java @@ -0,0 +1,77 @@ +package org.joinmastodon.android.ui.utils; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.R; +import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; +import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; + +public class MediaAttachmentViewController{ + public final View view; + public final MediaGridStatusDisplayItem.GridItemType type; + public final ImageView photo; + public final View altButton, noAltButton, btnsWrap; + public static int[] altWrapPadding = null; + private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); + private final Context context; + private boolean didClear; + private Status status; + + public MediaAttachmentViewController(Context context, MediaGridStatusDisplayItem.GridItemType type){ + view=context.getSystemService(LayoutInflater.class).inflate(switch(type){ + case PHOTO -> R.layout.display_item_photo; + case VIDEO -> R.layout.display_item_video; + case GIFV -> R.layout.display_item_gifv; + }, null); + photo=view.findViewById(R.id.photo); + altButton=view.findViewById(R.id.alt_button); + noAltButton=view.findViewById(R.id.no_alt_button); + btnsWrap=view.findViewById(R.id.alt_badges); + this.type=type; + this.context=context; + if (altWrapPadding == null) { + altWrapPadding = new int[] { btnsWrap.getPaddingLeft(), btnsWrap.getPaddingTop(), btnsWrap.getPaddingRight(), btnsWrap.getPaddingBottom() }; + } + } + + public void bind(Attachment attachment, Status status){ + this.status=status; + crossfadeDrawable.setSize(attachment.getWidth(), attachment.getHeight()); + crossfadeDrawable.setBlurhashDrawable(attachment.blurhashPlaceholder); + crossfadeDrawable.setCrossfadeAlpha(status.spoilerRevealed ? 0f : 1f); + photo.setImageDrawable(null); + photo.setImageDrawable(crossfadeDrawable); + boolean hasAltText = !TextUtils.isEmpty(attachment.description); + photo.setContentDescription(!hasAltText ? context.getString(R.string.media_no_description) : attachment.description); + if(btnsWrap!=null){ + btnsWrap.setVisibility(View.VISIBLE); + altButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); + noAltButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); + } + didClear=false; + } + + public void setImage(Drawable drawable){ + crossfadeDrawable.setImageDrawable(drawable); + if(didClear && status.spoilerRevealed) + crossfadeDrawable.animateAlpha(0f); + } + + public void clearImage(){ + crossfadeDrawable.setCrossfadeAlpha(1f); + crossfadeDrawable.setImageDrawable(null); + didClear=true; + } + + public void setRevealed(boolean revealed){ + crossfadeDrawable.animateAlpha(revealed ? 0f : 1f); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FrameLayoutThatOnlyMeasuresFirstChild.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FrameLayoutThatOnlyMeasuresFirstChild.java new file mode 100644 index 000000000..f195ac633 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FrameLayoutThatOnlyMeasuresFirstChild.java @@ -0,0 +1,29 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +public class FrameLayoutThatOnlyMeasuresFirstChild extends FrameLayout{ + public FrameLayoutThatOnlyMeasuresFirstChild(Context context){ + this(context, null); + } + + public FrameLayoutThatOnlyMeasuresFirstChild(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public FrameLayoutThatOnlyMeasuresFirstChild(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + if(getChildCount()==0) + return; + View child0=getChildAt(0); + measureChild(child0, widthMeasureSpec, heightMeasureSpec); + super.onMeasure(child0.getMeasuredWidth() | MeasureSpec.EXACTLY, child0.getMeasuredHeight() | MeasureSpec.EXACTLY); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ImageAttachmentFrameLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/ImageAttachmentFrameLayout.java deleted file mode 100644 index 0bfcbdb16..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ImageAttachmentFrameLayout.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.joinmastodon.android.ui.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; - -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import me.grishka.appkit.utils.V; - -public class ImageAttachmentFrameLayout extends FrameLayout{ - public static final int MAX_WIDTH=400; // dp - - private PhotoLayoutHelper.TiledLayoutResult tileLayout; - private PhotoLayoutHelper.TiledLayoutResult.Tile tile; - private int horizontalInset; - - public ImageAttachmentFrameLayout(@NonNull Context context){ - super(context); - } - - public ImageAttachmentFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs){ - super(context, attrs); - } - - public ImageAttachmentFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr){ - super(context, attrs, defStyleAttr); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ - if(isInEditMode()){ - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - int w=Math.min(((View)getParent()).getMeasuredWidth(), V.dp(MAX_WIDTH))-horizontalInset; - int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1); - int actualWidth=Math.round(tile.width/1000f*w); - if(tile.startCol+tile.colSpanmaxWidth){ + xOffset=(r-l)/2-maxWidth/2; + } + + for(int i=0;i{ + private final Function producer; + private final HashMap> pool=new HashMap<>(); + + public TypedObjectPool(Function producer){ + this.producer=producer; + } + + public V obtain(K type){ + LinkedList tp=pool.get(type); + if(tp==null) + pool.put(type, tp=new LinkedList<>()); + + V value=tp.poll(); + if(value==null) + value=producer.apply(type); + return value; + } + + public void reuse(K type, V obj){ + Objects.requireNonNull(obj); + Objects.requireNonNull(type); + + LinkedList tp=pool.get(type); + if(tp==null) + pool.put(type, tp=new LinkedList<>()); + tp.add(obj); + } +} diff --git a/mastodon/src/main/res/layout/alt_badge.xml b/mastodon/src/main/res/layout/alt_badge.xml index f165466cf..955f8f5cb 100644 --- a/mastodon/src/main/res/layout/alt_badge.xml +++ b/mastodon/src/main/res/layout/alt_badge.xml @@ -1,79 +1,34 @@ - - + - + - - - - - - - - - - - - - - - + diff --git a/mastodon/src/main/res/layout/display_item_gifv.xml b/mastodon/src/main/res/layout/display_item_gifv.xml index a3575ab06..a60f72ca8 100644 --- a/mastodon/src/main/res/layout/display_item_gifv.xml +++ b/mastodon/src/main/res/layout/display_item_gifv.xml @@ -1,5 +1,5 @@ - @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/mastodon/src/main/res/layout/display_item_photo.xml b/mastodon/src/main/res/layout/display_item_photo.xml index 462dab84a..71f4c5099 100644 --- a/mastodon/src/main/res/layout/display_item_photo.xml +++ b/mastodon/src/main/res/layout/display_item_photo.xml @@ -1,5 +1,6 @@ - @@ -12,4 +13,4 @@ - \ No newline at end of file + diff --git a/mastodon/src/main/res/layout/display_item_video.xml b/mastodon/src/main/res/layout/display_item_video.xml index 1f1283fab..17e1e40cd 100644 --- a/mastodon/src/main/res/layout/display_item_video.xml +++ b/mastodon/src/main/res/layout/display_item_video.xml @@ -1,5 +1,5 @@ - @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/mastodon/src/main/res/layout/overlay_image_alt_text.xml b/mastodon/src/main/res/layout/overlay_image_alt_text.xml new file mode 100644 index 000000000..f092abd87 --- /dev/null +++ b/mastodon/src/main/res/layout/overlay_image_alt_text.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + +