From 41682d1147078a3986d3f247167cb8a913693cb8 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 17:30:31 -0500 Subject: [PATCH 01/12] added press-and-hold listener to ClickableLinks --- .../ui/text/ClickableLinksDelegate.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) 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 482a7eacf..6b7573a5f 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 @@ -6,11 +6,14 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Handler; import android.text.Layout; import android.text.Spanned; import android.view.MotionEvent; import android.view.SoundEffectConstants; +import android.view.ViewConfiguration; import android.widget.TextView; +import android.widget.Toast; import me.grishka.appkit.utils.V; @@ -20,6 +23,7 @@ public class ClickableLinksDelegate { private Path hlPath; private LinkSpan selectedSpan; private TextView view; + private final Handler longClickHandler = new Handler(); public ClickableLinksDelegate(TextView view) { this.view=view; @@ -63,6 +67,7 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; + longClickHandler.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -90,21 +95,31 @@ public class ClickableLinksDelegate { } } if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ + longClickHandler.removeCallbacks(copyTextToClipboard); view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); - hlPath=null; - selectedSpan=null; - view.invalidate(); + resetAndInvalidate(); return false; } if(event.getAction()==MotionEvent.ACTION_CANCEL){ - hlPath=null; - selectedSpan=null; - view.invalidate(); + resetAndInvalidate(); return false; } return false; } + + Runnable copyTextToClipboard = () -> { + //TODO actually copy to clipboard + //TODO think about removing toast, system > A12 (?) has a built-in popup + Toast.makeText(view.getContext(), "copied to clipboard", Toast.LENGTH_SHORT).show(); + resetAndInvalidate(); + }; + + private void resetAndInvalidate() { + hlPath=null; + selectedSpan=null; + view.invalidate(); + } public void onDraw(Canvas canvas){ if(hlPath!=null){ From c0115f068c68fa9f0795a5dec0da2a74d2d62a08 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 17:40:43 -0500 Subject: [PATCH 02/12] implemented copy service --- .../android/ui/text/ClickableLinksDelegate.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 6b7573a5f..1684458e1 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 @@ -1,11 +1,15 @@ package org.joinmastodon.android.ui.text; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.graphics.Canvas; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Build; import android.os.Handler; import android.text.Layout; import android.text.Spanned; @@ -109,9 +113,15 @@ public class ClickableLinksDelegate { } Runnable copyTextToClipboard = () -> { - //TODO actually copy to clipboard - //TODO think about removing toast, system > A12 (?) has a built-in popup - Toast.makeText(view.getContext(), "copied to clipboard", Toast.LENGTH_SHORT).show(); + //copy link text to clipboard + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); + //show toast, android from S_V2 on has built-in popup, as documented in + //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(view.getContext(), "Copied", Toast.LENGTH_SHORT).show(); + } + //reset view resetAndInvalidate(); }; From e3486ebf7c673646ac7456c787ee17599a441eb5 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 18:00:45 -0500 Subject: [PATCH 03/12] added clickable link type switch for copy, to not copy hashtags and user IDs --- .../joinmastodon/android/ui/text/ClickableLinksDelegate.java | 2 ++ 1 file changed, 2 insertions(+) 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 1684458e1..4ae75ca3d 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 @@ -113,6 +113,8 @@ public class ClickableLinksDelegate { } Runnable copyTextToClipboard = () -> { + //if target is not a link, don't copy + if (selectedSpan.getType() != LinkSpan.Type.URL) return; //copy link text to clipboard ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); From 42c6446125595c4b00970e37ac8208fa18caac06 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 18:03:40 -0500 Subject: [PATCH 04/12] refactoring: moved runnable and made it private, added copy toast localization. --- .../ui/text/ClickableLinksDelegate.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) 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 4ae75ca3d..036d6f9af 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 @@ -19,6 +19,8 @@ import android.view.ViewConfiguration; import android.widget.TextView; import android.widget.Toast; +import org.joinmastodon.android.R; + import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { @@ -28,6 +30,21 @@ public class ClickableLinksDelegate { private LinkSpan selectedSpan; private TextView view; private final Handler longClickHandler = new Handler(); + + private final Runnable copyTextToClipboard = () -> { + //if target is not a link, don't copy + if (selectedSpan.getType() != LinkSpan.Type.URL) return; + //copy link text to clipboard + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); + //show toast, android from S_V2 on has built-in popup, as documented in + //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show(); + } + //reset view + resetAndInvalidate(); + }; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -112,21 +129,6 @@ public class ClickableLinksDelegate { return false; } - Runnable copyTextToClipboard = () -> { - //if target is not a link, don't copy - if (selectedSpan.getType() != LinkSpan.Type.URL) return; - //copy link text to clipboard - ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); - //show toast, android from S_V2 on has built-in popup, as documented in - //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(view.getContext(), "Copied", Toast.LENGTH_SHORT).show(); - } - //reset view - resetAndInvalidate(); - }; - private void resetAndInvalidate() { hlPath=null; selectedSpan=null; From f5df8225d1fb1039f312787d6ad5ddc5da24e0fc Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 18:20:02 -0500 Subject: [PATCH 05/12] whitespace corrections --- .../joinmastodon/android/ui/text/ClickableLinksDelegate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 036d6f9af..1906b2f39 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 @@ -45,7 +45,7 @@ public class ClickableLinksDelegate { //reset view resetAndInvalidate(); }; - + public ClickableLinksDelegate(TextView view) { this.view=view; hlPaint=new Paint(); @@ -134,7 +134,7 @@ public class ClickableLinksDelegate { selectedSpan=null; view.invalidate(); } - + public void onDraw(Canvas canvas){ if(hlPath!=null){ canvas.save(); From 794c4e52274b897036449ff39dd5aa8328510c07 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 19:54:08 -0500 Subject: [PATCH 06/12] removed longClickHandler and moved to view itself --- .../android/ui/text/ClickableLinksDelegate.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 1906b2f39..6035d2b51 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 @@ -10,7 +10,6 @@ import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; -import android.os.Handler; import android.text.Layout; import android.text.Spanned; import android.view.MotionEvent; @@ -29,7 +28,6 @@ public class ClickableLinksDelegate { private Path hlPath; private LinkSpan selectedSpan; private TextView view; - private final Handler longClickHandler = new Handler(); private final Runnable copyTextToClipboard = () -> { //if target is not a link, don't copy @@ -88,7 +86,7 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; - longClickHandler.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); + view.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -116,7 +114,7 @@ public class ClickableLinksDelegate { } } if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ - longClickHandler.removeCallbacks(copyTextToClipboard); + view.removeCallbacks(copyTextToClipboard); view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); resetAndInvalidate(); From fd99f3caa14bb1d471d67e17a15d9957b711526d Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 20:14:16 -0500 Subject: [PATCH 07/12] changed url longclick implementation to GestureListener --- .../ui/text/ClickableLinksDelegate.java | 94 +++++++++++-------- 1 file changed, 55 insertions(+), 39 deletions(-) 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 6035d2b51..300938f8a 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 @@ -12,12 +12,15 @@ import android.graphics.RectF; import android.os.Build; import android.text.Layout; import android.text.Spanned; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.ViewConfiguration; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; + import org.joinmastodon.android.R; import me.grishka.appkit.utils.V; @@ -29,20 +32,7 @@ public class ClickableLinksDelegate { private LinkSpan selectedSpan; private TextView view; - private final Runnable copyTextToClipboard = () -> { - //if target is not a link, don't copy - if (selectedSpan.getType() != LinkSpan.Type.URL) return; - //copy link text to clipboard - ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); - //show toast, android from S_V2 on has built-in popup, as documented in - //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show(); - } - //reset view - resetAndInvalidate(); - }; + GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -50,10 +40,34 @@ 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) { - if(event.getAction()==MotionEvent.ACTION_DOWN){ + if(event.getAction()==MotionEvent.ACTION_CANCEL){ + resetAndInvalidate(); + } + return gestureDetector.onTouchEvent(event); + } + + 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(); + } + } + + private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent event) { int line=-1; Rect rect=new Rect(); Layout l=view.getLayout(); @@ -112,34 +126,36 @@ public class ClickableLinksDelegate { } } } + return super.onDown(event); } - if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ - view.removeCallbacks(copyTextToClipboard); - view.playSoundEffect(SoundEffectConstants.CLICK); - selectedSpan.onClick(view.getContext()); - resetAndInvalidate(); + + @Override + public boolean onSingleTapUp(@NonNull MotionEvent event) { + if(selectedSpan!=null){ + view.removeCallbacks(copyTextToClipboard); + view.playSoundEffect(SoundEffectConstants.CLICK); + selectedSpan.onClick(view.getContext()); + resetAndInvalidate(); + return true; + } return false; } - if(event.getAction()==MotionEvent.ACTION_CANCEL){ + + @Override + public void onLongPress(@NonNull MotionEvent event) { + //if target is not a link, don't copy + if (selectedSpan == null) return; + if (selectedSpan.getType() != LinkSpan.Type.URL) return; + //copy link text to clipboard + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); + //show toast, android from S_V2 on has built-in popup, as documented in + //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show(); + } + //reset view resetAndInvalidate(); - return false; - } - return false; - } - - 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(); } } - } From 0a8d73dc0bd8e5fd517f4e1654771c9928b4a466 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 20:19:10 -0500 Subject: [PATCH 08/12] cleanup, resolved some warnings --- .../android/ui/text/ClickableLinksDelegate.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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 300938f8a..be9921907 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 @@ -15,7 +15,6 @@ import android.text.Spanned; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.SoundEffectConstants; -import android.view.ViewConfiguration; import android.widget.TextView; import android.widget.Toast; @@ -27,12 +26,12 @@ import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { - private Paint hlPaint; + final private Paint hlPaint; private Path hlPath; private LinkSpan selectedSpan; - private TextView view; + final private TextView view; - GestureDetector gestureDetector; + final GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -82,8 +81,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){ @@ -100,7 +98,6 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; - view.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -132,7 +129,6 @@ public class ClickableLinksDelegate { @Override public boolean onSingleTapUp(@NonNull MotionEvent event) { if(selectedSpan!=null){ - view.removeCallbacks(copyTextToClipboard); view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); resetAndInvalidate(); From 4144639b7564a9d3be9306f165c8be63e493b9ae Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Sun, 26 Feb 2023 13:59:39 -0500 Subject: [PATCH 09/12] docu --- .../android/ui/text/ClickableLinksDelegate.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 be9921907..9dac109c0 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 @@ -44,11 +44,16 @@ public class ClickableLinksDelegate { public boolean onTouch(MotionEvent event) { 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; @@ -64,6 +69,12 @@ public class ClickableLinksDelegate { } } + /** + * 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) { From f79fc66578d350d5323ed0be08d5e67062d87e90 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 5 Mar 2023 22:33:18 +0300 Subject: [PATCH 10/12] Fix --- .../android/ui/text/ClickableLinksDelegate.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 9dac109c0..e7b219940 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 @@ -26,12 +26,12 @@ import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { - final private Paint hlPaint; + private final Paint hlPaint; private Path hlPath; private LinkSpan selectedSpan; - final private TextView view; + private final TextView view; - final GestureDetector gestureDetector; + private final GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view; From 4a695b2a83a52b0450637b9952ae80ba57468ac7 Mon Sep 17 00:00:00 2001 From: Grishka Date: Mon, 6 Mar 2023 02:25:13 +0300 Subject: [PATCH 11/12] Use a single display item for the image attachment grid --- .../fragments/BaseStatusListFragment.java | 156 +++------- .../android/fragments/ComposeFragment.java | 4 +- .../fragments/NotificationsListFragment.java | 8 - .../report/ReportAddPostsChoiceFragment.java | 33 +- .../android/ui/PhotoLayoutHelper.java | 17 +- .../displayitems/GifVStatusDisplayItem.java | 42 --- .../displayitems/ImageStatusDisplayItem.java | 105 ------- .../MediaGridStatusDisplayItem.java | 292 ++++++++++++++++++ .../displayitems/PhotoStatusDisplayItem.java | 144 --------- .../ui/displayitems/StatusDisplayItem.java | 41 ++- .../displayitems/VideoStatusDisplayItem.java | 42 --- .../ui/photoviewer/PhotoViewerHost.java | 3 +- .../ui/utils/InsetStatusItemDecoration.java | 15 +- .../utils/MediaAttachmentViewController.java | 67 ++++ ...FrameLayoutThatOnlyMeasuresFirstChild.java | 29 ++ .../ui/views/ImageAttachmentFrameLayout.java | 54 ---- .../android/ui/views/MediaGridLayout.java | 108 +++++++ .../{ui => }/utils/TransferSpeedTracker.java | 2 +- .../android/utils/TypedObjectPool.java | 36 +++ .../src/main/res/layout/display_item_gifv.xml | 4 +- .../main/res/layout/display_item_photo.xml | 66 +--- .../main/res/layout/display_item_video.xml | 4 +- .../res/layout/overlay_image_alt_text.xml | 58 ++++ 23 files changed, 679 insertions(+), 651 deletions(-) delete mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GifVStatusDisplayItem.java delete mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java delete mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java delete mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/VideoStatusDisplayItem.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/FrameLayoutThatOnlyMeasuresFirstChild.java delete mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/ImageAttachmentFrameLayout.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/MediaGridLayout.java rename mastodon/src/main/java/org/joinmastodon/android/{ui => }/utils/TransferSpeedTracker.java (96%) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/utils/TypedObjectPool.java create mode 100644 mastodon/src/main/res/layout/overlay_image_alt_text.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 32faa1c90..8587ebc5a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -29,20 +29,20 @@ 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; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; 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; @@ -53,7 +53,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.api.Callback; import me.grishka.appkit.api.ErrorResponse; @@ -73,6 +72,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); @@ -171,21 +171,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; @@ -193,7 +193,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; @@ -220,15 +221,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; @@ -244,23 +246,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); } }); } @@ -310,31 +297,6 @@ public abstract class BaseStatusListFragment exten updateToolbar(); } - @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()); } @@ -479,13 +441,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++; @@ -609,6 +572,14 @@ public abstract class BaseStatusListFragment exten currentPhotoViewer.onPause(); } + 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(){ @@ -646,16 +617,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(); @@ -732,47 +689,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;i items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null); - 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; 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 c7b84c2ec..73c80ed70 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 @@ -25,7 +25,6 @@ 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; @@ -131,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); @@ -165,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); - 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 782f94e17..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.app.Activity; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -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; - -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; - - 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; - } - - @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; - } - - @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..5f0daeb3e --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java @@ -0,0 +1,292 @@ +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.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.TextView; + +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 View altTextScroller; + private final ImageButton altTextClose; + private final TextView altText; + + 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); + altTextScroller=findViewById(R.id.alt_text_scroller); + altTextClose=findViewById(R.id.alt_text_close); + altText=findViewById(R.id.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.altButton!=null){ + c.altButton.setOnClickListener(altTextClickListener); + c.altButton.setTag(i); + c.altButton.setAlpha(1f); + } + controllers.add(c); + c.bind(att, item.status); + i++; + } + 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); + altText.setText(att.description); + altTextWrapper.setVisibility(View.VISIBLE); + 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(altTextScroller, View.ALPHA, 0, 1)); + anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL, altTextWrapper.getLeft())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT, altTextWrapper.getTop())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth(), altTextWrapper.getRight())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight(), altTextWrapper.getBottom())); + for(Animator a:anims) + a.setDuration(300); + + for(MediaAttachmentViewController c:controllers){ + if(c.altButton!=null && c.altButton!=v){ + anims.add(ObjectAnimator.ofFloat(c.altButton, 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.altButton!=null){ + c.altButton.setVisibility(View.INVISIBLE); + } + } + } + }); + altTextAnimator=set; + set.start(); + + return true; + } + }); + } + + private void onAltTextCloseClick(View v){ + if(altTextAnimator!=null) + altTextAnimator.cancel(); + + View btn=controllers.get(altTextIndex).altButton; + for(MediaAttachmentViewController c:controllers){ + if(c.altButton!=null && c.altButton!=btn) + c.altButton.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(altTextScroller, View.ALPHA, 0)); + anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight())); + for(Animator a:anims) + a.setDuration(300); + + for(MediaAttachmentViewController c:controllers){ + if(c.altButton!=null && c.altButton!=btn){ + anims.add(ObjectAnimator.ofFloat(c.altButton, 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 104b02fb6..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java +++ /dev/null @@ -1,144 +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.ScrollView; -import android.widget.TextView; - -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; - -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{ - private final FrameLayout altTextWrapper; - private final TextView altTextButton; - private final View altTextScroller; - private final ImageButton altTextClose; - private final TextView altText; - - private boolean altTextShown; - private AnimatorSet currentAnim; - - public Holder(Activity activity, ViewGroup parent){ - super(activity, R.layout.display_item_photo, parent); - altTextWrapper=findViewById(R.id.alt_text_wrapper); - altTextButton=findViewById(R.id.alt_button); - altTextScroller=findViewById(R.id.alt_text_scroller); - altTextClose=findViewById(R.id.alt_text_close); - altText=findViewById(R.id.alt_text); - - altTextButton.setOnClickListener(this::onShowHideClick); - altTextClose.setOnClickListener(this::onShowHideClick); -// altTextScroller.setNestedScrollingEnabled(true); - } - - @Override - public void onBind(ImageStatusDisplayItem item){ - super.onBind(item); - altTextShown=false; - if(currentAnim!=null) - currentAnim.cancel(); - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - altTextButton.setVisibility(View.VISIBLE); - altTextButton.setAlpha(1f); - if(TextUtils.isEmpty(item.attachment.description)){ - altTextWrapper.setVisibility(View.GONE); - }else{ - altTextWrapper.setVisibility(View.VISIBLE); - altText.setText(item.attachment.description); - } - } - - private void onShowHideClick(View v){ - boolean show=v.getId()==R.id.alt_button; - - if(altTextShown==show) - return; - if(currentAnim!=null) - currentAnim.cancel(); - - altTextShown=show; - if(show){ - altTextScroller.setVisibility(View.VISIBLE); - altTextClose.setVisibility(View.VISIBLE); - }else{ - altTextButton.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(altTextButton, 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){ - altTextButton.setVisibility(View.GONE); - }else{ - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - } - currentAnim=null; - } - }); - set.start(); - currentAnim=set; - - return true; - } - }); - } - } -} 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 d1e5e1fd8..adedc0e8e 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 @@ -53,10 +53,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); @@ -66,6 +63,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); }; } @@ -87,20 +85,23 @@ 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)); + +// int photoIndex=0; +// 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++; +// } } for(Attachment att:statusForContent.mediaAttachments){ if(att.type==Attachment.Type.AUDIO){ @@ -137,9 +138,6 @@ public abstract class StatusDisplayItem{ HEADER, REBLOG_OR_REPLY_LINE, TEXT, - PHOTO, - VIDEO, - GIFV, AUDIO, POLL_OPTION, POLL_FOOTER, @@ -149,7 +147,8 @@ public abstract class StatusDisplayItem{ ACCOUNT, HASHTAG, GAP, - EXTENDED_FOOTER + EXTENDED_FOOTER, + MEDIA_GRID } 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/utils/InsetStatusItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java index a811fb464..12581b09c 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,9 @@ 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 +86,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..9f269a2c8 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java @@ -0,0 +1,67 @@ +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.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; + 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); + this.type=type; + this.context=context; + } + + 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); + photo.setContentDescription(TextUtils.isEmpty(attachment.description) ? context.getString(R.string.media_no_description) : attachment.description); + if(altButton!=null){ + altButton.setVisibility(TextUtils.isEmpty(attachment.description) ? View.GONE : View.VISIBLE); + } + 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/display_item_gifv.xml b/mastodon/src/main/res/layout/display_item_gifv.xml index 4dccf3eff..8f373c682 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 @@ - @@ -25,4 +25,4 @@ android:layout_margin="8dp" android:background="@drawable/ic_gif"/> - \ No newline at end of file + \ 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 00b0bd176..2ba0d2860 100644 --- a/mastodon/src/main/res/layout/display_item_photo.xml +++ b/mastodon/src/main/res/layout/display_item_photo.xml @@ -1,5 +1,5 @@ - @@ -12,61 +12,21 @@ android:scaleType="centerCrop"/> - - - + android:importantForAccessibility="no" + android:textAppearance="@style/m3_label_large" + android:textColor="#FFF" + android:gravity="center" + android:includeFontPadding="false" + android:background="@drawable/bg_image_alt_overlay" + android:text="ALT"/> - - - - - - - - - - - - - - - \ No newline at end of file + \ 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 83e8112d9..5ecaa5782 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 @@ - @@ -18,4 +18,4 @@ android:elevation="3dp" android:background="@drawable/play_button"/> - \ No newline at end of file + \ 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..53f4e5ba6 --- /dev/null +++ b/mastodon/src/main/res/layout/overlay_image_alt_text.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + From c3aa3af6507eed33b965c9da85a0da3b7047c2e1 Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 8 Mar 2023 22:46:24 +0300 Subject: [PATCH 12/12] Fix #540 --- .../android/fragments/ComposeFragment.java | 8 +++++--- .../ui/displayitems/StatusDisplayItem.java | 15 --------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 0145642e5..371e5b02f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -640,9 +640,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } private void onCustomEmojiClick(Emoji emoji){ - int start=mainEditText.getSelectionStart(); - String prefix=start>0 && !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/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index adedc0e8e..783c2f3f0 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 @@ -86,22 +86,7 @@ public abstract class StatusDisplayItem{ List imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); if(!imageAttachments.isEmpty()){ PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments); - items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent)); - -// int photoIndex=0; -// 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++; -// } } for(Attachment att:statusForContent.mediaAttachments){ if(att.type==Attachment.Type.AUDIO){