diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index 054b7a7dd..3f3b618e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MessageRecordUtil; import org.thoughtcrime.securesms.util.Projection; +import org.thoughtcrime.securesms.util.ProjectionList; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ViewUtil; @@ -704,7 +705,7 @@ public class ConversationAdapter } @Override - public @NonNull List getColorizerProjections(@NonNull ViewGroup coordinateRoot) { + public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) { return getBindable().getColorizerProjections(coordinateRoot); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index a2bf5123f..4c1ebaf01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -122,6 +122,7 @@ import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan; import org.thoughtcrime.securesms.util.LongClickMovementMethod; import org.thoughtcrime.securesms.util.MessageRecordUtil; import org.thoughtcrime.securesms.util.Projection; +import org.thoughtcrime.securesms.util.ProjectionList; import org.thoughtcrime.securesms.util.SearchUtil; import org.thoughtcrime.securesms.util.StringUtil; import org.thoughtcrime.securesms.util.ThemeUtil; @@ -221,6 +222,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private Colorizer colorizer; private boolean hasWallpaper; private float lastYDownRelativeToThis; + private ProjectionList colorizerProjections = new ProjectionList(3); public ConversationItem(Context context) { this(context, null); @@ -530,6 +532,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo if (conversationRecipient != null) { conversationRecipient.removeForeverObserver(this); } + + bodyBubble.setVideoPlayerProjection(null); + bodyBubble.setQuoteViewProjection(null); + cancelPulseOutlinerAnimation(); } @@ -588,12 +594,17 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } private static int getProjectionTop(@NonNull View child) { - return (int) Projection.relativeToViewRoot(child, null).getY(); + Projection projection = Projection.relativeToViewRoot(child, null); + int y = (int) projection.getY(); + projection.release(); + return y; } private static int getProjectionBottom(@NonNull View child) { Projection projection = Projection.relativeToViewRoot(child, null); - return (int) projection.getY() + projection.getHeight(); + int bottom = (int) projection.getY() + projection.getHeight(); + projection.release(); + return bottom; } @Override @@ -1719,8 +1730,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } @Override - public @NonNull List getColorizerProjections(@NonNull ViewGroup coordinateRoot) { - List projections = new LinkedList<>(); + public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) { + colorizerProjections.clear(); if (messageRecord.isOutgoing() && !hasNoBubble(messageRecord) && @@ -1731,9 +1742,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo Projection videoToBubble = bodyBubble.getVideoPlayerProjection(); if (videoToBubble != null) { Projection videoToRoot = Projection.translateFromDescendantToParentCoords(videoToBubble, bodyBubble, coordinateRoot); - projections.addAll(Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot)); + colorizerProjections.addAll(Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot)); } else { - projections.add(bodyBubbleToRoot); + colorizerProjections.add(bodyBubbleToRoot); } } @@ -1743,7 +1754,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo { Projection footerProjection = getActiveFooter(messageRecord).getProjection(coordinateRoot); if (footerProjection != null) { - projections.add(footerProjection.translateX(bodyBubble.getTranslationX())); + colorizerProjections.add(footerProjection.translateX(bodyBubble.getTranslationX())); } } @@ -1752,10 +1763,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo quoteView != null) { bodyBubble.setQuoteViewProjection(quoteView.getProjection(bodyBubble)); - projections.add(quoteView.getProjection(coordinateRoot).translateX(bodyBubble.getTranslationX() + this.getTranslationX())); + colorizerProjections.add(quoteView.getProjection(coordinateRoot).translateX(bodyBubble.getTranslationX() + this.getTranslationX())); } - return projections.stream().map(p -> p.translateY(this.getTranslationY())).collect(Collectors.toList()); + for (int i = 0; i < colorizerProjections.size(); i++) { + colorizerProjections.get(i).translateY(getTranslationY()); + } + + return colorizerProjections; } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java index c218f1a5e..3b23701ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java @@ -63,11 +63,19 @@ public class ConversationItemBodyBubble extends LinearLayout { } public void setQuoteViewProjection(@Nullable Projection quoteViewProjection) { + if (this.quoteViewProjection != null) { + this.quoteViewProjection.release(); + } + this.quoteViewProjection = quoteViewProjection; clipProjectionDrawable.setProjections(getProjections()); } public void setVideoPlayerProjection(@Nullable Projection videoPlayerProjection) { + if (this.videoPlayerProjection != null) { + this.videoPlayerProjection.release(); + } + this.videoPlayerProjection = videoPlayerProjection; clipProjectionDrawable.setProjections(getProjections()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 8e2d2f74f..69a0c2d25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.Projection; +import org.thoughtcrime.securesms.util.ProjectionList; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; @@ -59,7 +60,9 @@ import java.util.concurrent.ExecutionException; public final class ConversationUpdateItem extends FrameLayout implements BindableConversationItem { - private static final String TAG = Log.tag(ConversationUpdateItem.class); + private static final String TAG = Log.tag(ConversationUpdateItem.class); + private static final ProjectionList EMPTY_PROJECTION_LIST = new ProjectionList(); + private Set batchSelected; @@ -221,8 +224,8 @@ public final class ConversationUpdateItem extends FrameLayout } @Override - public @NonNull List getColorizerProjections(@NonNull ViewGroup coordinateRoot) { - return Collections.emptyList(); + public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) { + return EMPTY_PROJECTION_LIST; } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizable.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizable.kt index 9e7600747..4825952c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizable.kt @@ -1,12 +1,12 @@ package org.thoughtcrime.securesms.conversation.colors import android.view.ViewGroup -import org.thoughtcrime.securesms.util.Projection +import org.thoughtcrime.securesms.util.ProjectionList /** * Denotes that a class can be colorized. The class is responsible for * generating its own projection. */ interface Colorizable { - fun getColorizerProjections(coordinateRoot: ViewGroup): List + fun getColorizerProjections(coordinateRoot: ViewGroup): ProjectionList } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ColorizerView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ColorizerView.kt index 7e5eea867..6e9df85fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ColorizerView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ColorizerView.kt @@ -40,4 +40,9 @@ class ColorizerView @JvmOverloads constructor( super.draw(canvas) } } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + projections.forEach { it.release() } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt index 7a0aba26b..184c73408 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt @@ -98,8 +98,10 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) { if (child != null) { val holder = parent.getChildViewHolder(child) if (holder is Colorizable) { - holder.getColorizerProjections(parent).forEach { - c.drawPath(it.path, holePunchPaint) + holder.getColorizerProjections(parent).use { list -> + list.forEach { + c.drawPath(it.path, holePunchPaint) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt index e915337bb..f6ae29a29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt @@ -21,7 +21,6 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.lottie.SimpleColorFilter import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.conversation.ConversationAdapter -import org.thoughtcrime.securesms.util.Projection import org.thoughtcrime.securesms.util.SetUtil import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -157,9 +156,17 @@ class MultiselectItemDecoration( val parts: MultiselectCollection = child.conversationMessage.multiselectCollection - val projections: List = child.getColorizerProjections(parent) + if (child.canPlayContent()) listOf(child.getGiphyMp4PlayableProjection(parent)) else emptyList() + val projections = child.getColorizerProjections(parent) + if (child.canPlayContent()) { + projections.add(child.getGiphyMp4PlayableProjection(parent)) + } + path.reset() - projections.forEach { it.applyToPath(path) } + projections.use { list -> + list.forEach { + it.applyToPath(path) + } + } canvas.save() canvas.clipPath(path, Region.Op.DIFFERENCE) @@ -341,13 +348,16 @@ class MultiselectItemDecoration( parent.forEach { child -> if (child is Multiselectable && child.conversationMessage == inFocus.conversationMessage) { path.addRect(child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat(), Path.Direction.CW) - child.getColorizerProjections(parent).forEach { - path.op(it.path, Path.Op.DIFFERENCE) + child.getColorizerProjections(parent).use { list -> + list.forEach { + path.op(it.path, Path.Op.DIFFERENCE) + } } if (child.canPlayContent()) { val mp4GifProjection = child.getGiphyMp4PlayableProjection(child.rootView as ViewGroup) path.op(mp4GifProjection.path, Path.Op.DIFFERENCE) + mp4GifProjection.release() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java index 3dad6001a..e77f8d659 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java @@ -105,6 +105,8 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl } holder.setCorners(projection.getCorners()); + + projection.release(); } private void startPlayback(@NonNull RecyclerView parent, @NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java index 1151a7247..78fc08ac8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.Projection; +import org.thoughtcrime.securesms.util.ProjectionList; import org.whispersystems.libsignal.util.guava.Optional; import java.sql.Date; @@ -250,7 +251,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G } @Override - public @NonNull List getColorizerProjections(@NonNull ViewGroup coordinateRoot) { + public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) { return conversationItem.getColorizerProjections(coordinateRoot); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java b/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java index 3e9d07ecb..189b26810 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java @@ -9,6 +9,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pools; import androidx.recyclerview.widget.RecyclerView; import org.signal.core.util.logging.Log; @@ -24,30 +25,41 @@ import java.util.Objects; */ public final class Projection { - private final float x; - private final float y; - private final int width; - private final int height; - private final Corners corners; - private final Path path; - private final RectF rect; + private float x; + private float y; + private int width; + private int height; + private Corners corners; + private Path path; + private RectF rect; - public Projection(float x, float y, int width, int height, @Nullable Corners corners) { + private Projection() { + x = 0f; + y = 0f; + width = 0; + height = 0; + corners = null; + path = new Path(); + rect = new RectF(); + } + + private Projection set(float x, float y, int width, int height, @Nullable Corners corners) { this.x = x; this.y = y; this.width = width; this.height = height; this.corners = corners; - this.path = new Path(); - rect = new RectF(); rect.set(x, y, x + width, y + height); + path.reset(); if (corners != null) { path.addRoundRect(rect, corners.toRadii(), Path.Direction.CW); } else { path.addRect(rect, Path.Direction.CW); } + + return this; } public float getX() { @@ -86,35 +98,12 @@ public final class Projection { } } - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final Projection that = (Projection) o; - return Float.compare(that.x, x) == 0 && - Float.compare(that.y, y) == 0 && - width == that.width && - height == that.height && - Objects.equals(corners, that.corners); - } - - @Override public int hashCode() { - return Objects.hash(x, y, width, height, corners); - } - public @NonNull Projection translateX(float xTranslation) { - return new Projection(x + xTranslation, y, width, height, corners); + return set(x + xTranslation, y, width, height, corners); } public @NonNull Projection translateY(float yTranslation) { - return new Projection(x, y + yTranslation, width, height, corners); - } - - public @NonNull Projection withDimensions(int width, int height) { - return new Projection(x, y, width, height, corners); - } - - public @NonNull Projection withHeight(int height) { - return new Projection(x, y, width, height, corners); + return set(x, y + yTranslation, width, height, corners); } public static @NonNull Projection relativeToParent(@NonNull ViewGroup parent, @NonNull View view, @Nullable Corners corners) { @@ -122,7 +111,7 @@ public final class Projection { view.getDrawingRect(viewBounds); parent.offsetDescendantRectToMyCoords(view, viewBounds); - return new Projection(viewBounds.left, viewBounds.top, view.getWidth(), view.getHeight(), corners); + return acquireAndSet(viewBounds.left, viewBounds.top, view.getWidth(), view.getHeight(), corners); } public static @NonNull Projection relativeToViewRoot(@NonNull View view, @Nullable Corners corners) { @@ -132,7 +121,7 @@ public final class Projection { view.getDrawingRect(viewBounds); root.offsetDescendantRectToMyCoords(view, viewBounds); - return new Projection(viewBounds.left, viewBounds.top, view.getWidth(), view.getHeight(), corners); + return acquireAndSet(viewBounds.left, viewBounds.top, view.getWidth(), view.getHeight(), corners); } public static @NonNull Projection relativeToViewWithCommonRoot(@NonNull View toProject, @NonNull View viewWithCommonRoot, @Nullable Corners corners) { @@ -143,7 +132,7 @@ public final class Projection { root.offsetDescendantRectToMyCoords(toProject, viewBounds); root.offsetRectIntoDescendantCoords(viewWithCommonRoot, viewBounds); - return new Projection(viewBounds.left, viewBounds.top, toProject.getWidth(), toProject.getHeight(), corners); + return acquireAndSet(viewBounds.left, viewBounds.top, toProject.getWidth(), toProject.getHeight(), corners); } public static @NonNull Projection translateFromDescendantToParentCoords(@NonNull Projection descendantProjection, @NonNull View descendant, @NonNull ViewGroup parent) { @@ -153,7 +142,7 @@ public final class Projection { parent.offsetDescendantRectToMyCoords(descendant, viewBounds); - return new Projection(viewBounds.left, viewBounds.top, descendantProjection.width, descendantProjection.height, descendantProjection.corners); + return acquireAndSet(viewBounds.left, viewBounds.top, descendantProjection.width, descendantProjection.height, descendantProjection.corners); } public static @NonNull List getCapAndTail(@NonNull Projection parentProjection, @NonNull Projection childProjection) { @@ -187,11 +176,47 @@ public final class Projection { } return Arrays.asList( - new Projection(topX, topY, topWidth, topHeight, topCorners), - new Projection(bottomX, bottomY, bottomWidth, bottomHeight, bottomCorners) + acquireAndSet(topX, topY, topWidth, topHeight, topCorners), + acquireAndSet(bottomX, bottomY, bottomWidth, bottomHeight, bottomCorners) ); } + /** + * We keep a maximum of 125 Projections around at any one time. + */ + private static final Pools.SimplePool projectionPool = new Pools.SimplePool<>(125); + + /** + * Acquire a projection. This will try to grab one from the pool, and, upon failure, will + * allocate a new one instead. + */ + private static @NonNull Projection acquire() { + Projection fromPool = projectionPool.acquire(); + if (fromPool != null) { + return fromPool; + } else { + return new Projection(); + } + } + + /** + * Acquire a projection and set its fields as specified. + */ + private static @NonNull Projection acquireAndSet(float x, float y, int width, int height, @Nullable Corners corners) { + Projection projection = acquire(); + projection.set(x, y, width, height, corners); + + return projection; + } + + /** + * Projections should only be kept around for the absolute maximum amount of time they are needed. + */ + public void release() { + projectionPool.release(this); + } + + public static final class Corners { private final float topLeft; private final float topRight; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProjectionList.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ProjectionList.kt new file mode 100644 index 000000000..500c5ddaa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProjectionList.kt @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.util + +import java.io.Closeable + +class ProjectionList(size: Int = 0) : ArrayList(size), Closeable { + override fun close() { + forEach { it.release() } + } +}