From f4cf9b47f4abb1a5ded5e2d39a53af00a5348aa1 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Fri, 28 Mar 2025 02:54:13 -0700 Subject: [PATCH 01/23] Flatten View hierarchy in attachment keyboard There was a ConstraintLayout that wrapped another ConstraintLayout and had no other siblings. I removed it, recompiled, and it worked, so I figure it's not necessary. This improves performance. --- .../layout/attachment_keyboard_fragment.xml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/layout/attachment_keyboard_fragment.xml b/app/src/main/res/layout/attachment_keyboard_fragment.xml index 8499994c8ed..5c45a9300c2 100644 --- a/app/src/main/res/layout/attachment_keyboard_fragment.xml +++ b/app/src/main/res/layout/attachment_keyboard_fragment.xml @@ -2,19 +2,7 @@ ~ Copyright 2023 Signal Messenger, LLC ~ SPDX-License-Identifier: AGPL-3.0-only --> - - - - - + android:layout_height="match_parent" /> \ No newline at end of file From 27128bd2dc2433045a7d49fbe9f2fe335e41a95d Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Fri, 11 Apr 2025 03:06:33 -0700 Subject: [PATCH 02/23] Debug shit --- .../components/InsetAwareConstraintLayout.kt | 29 +++++- ...ttachment_keyboard_nestedlinearlayouts.xml | 84 ++++++++++++++++ .../attachment_keyboard_relativelayout.xml | 97 +++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml create mode 100644 app/src/main/res/layout/attachment_keyboard_relativelayout.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index 7feca1e6745..c6d4045f839 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.components +import android.animation.ValueAnimator import android.content.Context import android.os.Build import android.util.AttributeSet @@ -63,6 +64,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( private val displayMetrics = DisplayMetrics() private var overridingKeyboard: Boolean = false private var previousKeyboardHeight: Int = 0 + private var otherKeyboardAnimator: ValueAnimator? = null val isKeyboardShowing: Boolean get() = previousKeyboardHeight > 0 @@ -123,6 +125,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } else if (!overridingKeyboard) { if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(windowInsets.bottom) + // animateKeyboardGuidelineTo(windowInsets.bottom) } else { keyboardAnimator.endingGuidelineEnd = windowInsets.bottom } @@ -143,7 +146,8 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun overrideKeyboardGuidelineWithPreviousHeight() { overridingKeyboard = true - keyboardGuideline?.setGuidelineEnd(getKeyboardHeight()) + // keyboardGuideline?.setGuidelineEnd(getKeyboardHeight()) + animateKeyboardGuidelineTo(getKeyboardHeight()) } protected fun clearKeyboardGuidelineOverride() { @@ -152,7 +156,28 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun resetKeyboardGuideline() { clearKeyboardGuidelineOverride() - keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd) + // keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd) + animateKeyboardGuidelineTo(navigationBarGuideline.guidelineEnd) + } + + private fun animateKeyboardGuidelineTo(target: Int) { + if (keyboardGuideline != null) { + otherKeyboardAnimator?.end() + otherKeyboardAnimator = null + } + otherKeyboardAnimator = ValueAnimator.ofInt(target, target, keyboardGuideline.guidelineEnd, target, keyboardGuideline.guidelineEnd, target).apply { + duration = 10_000 + addUpdateListener { animation -> + (animation.animatedValue as? Int)?.let { currentValue -> + keyboardGuideline?.setGuidelineEnd(currentValue) + forceLayout() + requestLayout() + invalidate() + forceLayout() + } + } + start() + } } private fun getKeyboardHeight(): Int { diff --git a/app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml b/app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml new file mode 100644 index 00000000000..5fbc1891e58 --- /dev/null +++ b/app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/attachment_keyboard_relativelayout.xml b/app/src/main/res/layout/attachment_keyboard_relativelayout.xml new file mode 100644 index 00000000000..0908a6cd53f --- /dev/null +++ b/app/src/main/res/layout/attachment_keyboard_relativelayout.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file From fb46cd93474232ea9e5cf0f4d40dbd18c81df26b Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Wed, 25 Jun 2025 05:52:26 -0700 Subject: [PATCH 03/23] Use LinearLayout for attachment keyboard buttons RecyclerView introduces excessive boilerplate for a container whose size is roughly fixed at a small number and is generally expected to not need recycling to be done. A simple horizontal LinearLayout wrapped in a HorizontalScrollView for good measure simplifies the code and reduces weird interactions with the recycler when the attachment keyboard is resized as part of the smooth open/close animation that I am adding. --- .../components/ButtonStripItemView.kt | 7 ++ .../AttachmentButtonCenterHelper.kt | 12 +-- .../conversation/AttachmentKeyboard.java | 20 ++-- .../AttachmentKeyboardButtonAdapter.java | 99 ------------------- .../AttachmentKeyboardButtonList.kt | 49 +++++++++ .../main/res/layout/attachment_keyboard.xml | 4 +- .../attachment_keyboard_button_list.xml | 13 +++ 7 files changed, 82 insertions(+), 122 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt create mode 100644 app/src/main/res/layout/attachment_keyboard_button_list.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt index 33fef7c6e22..b084527994f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt @@ -8,6 +8,7 @@ import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.conversation.AttachmentKeyboardButton class ButtonStripItemView @JvmOverloads constructor( context: Context, @@ -42,4 +43,10 @@ class ButtonStripItemView @JvmOverloads constructor( fun setOnIconClickedListener(onIconClickedListener: (() -> Unit)?) { iconView.setOnClickListener { onIconClickedListener?.invoke() } } + + /** Fills the icon/label from the given button enum */ + fun fillFromButton(button: AttachmentKeyboardButton) { + iconView.setImageResource(button.iconRes) + labelView.setText(button.titleRes) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt index 7087564c72a..8787620e694 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt @@ -1,23 +1,23 @@ package org.thoughtcrime.securesms.conversation +import android.view.ViewGroup import androidx.core.view.doOnNextLayout -import androidx.recyclerview.widget.RecyclerView import org.signal.core.util.DimensionUnit /** - * Adds necessary padding to each side of the given RecyclerView in order to ensure that + * Adds necessary padding to each side of the given ViewGroup in order to ensure that * if all buttons can fit in the visible real-estate on screen, they are centered. */ -class AttachmentButtonCenterHelper(private val recyclerView: RecyclerView) : RecyclerView.AdapterDataObserver() { +object AttachmentButtonCenterHelper { private val itemWidth: Float = DimensionUnit.DP.toPixels(88f) private val defaultPadding: Float = DimensionUnit.DP.toPixels(16f) - override fun onChanged() { - val itemCount = recyclerView.adapter?.itemCount ?: return + fun recenter(container: ViewGroup) { + val itemCount = container.childCount val requiredSpace = itemWidth * itemCount - recyclerView.doOnNextLayout { + container.doOnNextLayout { if (it.measuredWidth >= requiredSpace) { val extraSpace = it.measuredWidth - requiredSpace val availablePadding = extraSpace / 2f diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java index c16ae1037dd..30e690eea40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java @@ -12,7 +12,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; @@ -42,9 +41,9 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout. private View container; private AttachmentKeyboardMediaAdapter mediaAdapter; - private AttachmentKeyboardButtonAdapter buttonAdapter; private Callback callback; + private AttachmentKeyboardButtonList buttonList; private RecyclerView mediaList; private TextView permissionText; private MaterialButton permissionButton; @@ -69,16 +68,14 @@ private void init(@NonNull Context context) { this.permissionButton = findViewById(R.id.attachment_keyboard_permission_button); this.manageButton = findViewById(R.id.attachment_keyboard_manage_button); - RecyclerView buttonList = findViewById(R.id.attachment_keyboard_button_list); - buttonList.setItemAnimator(null); - mediaAdapter = new AttachmentKeyboardMediaAdapter(Glide.with(this), media -> { if (callback != null) { callback.onAttachmentMediaClicked(media); } }); - buttonAdapter = new AttachmentKeyboardButtonAdapter(button -> { + buttonList = findViewById(R.id.attachment_keyboard_button_list); + buttonList.setOnButtonClickedCallback(button -> { if (callback != null) { callback.onAttachmentSelectorClicked(button); } @@ -95,14 +92,10 @@ private void init(@NonNull Context context) { mediaList.setAdapter(mediaAdapter); mediaList.addOnScrollListener(new ScrollListener(manageButton.getMeasuredWidth())); - buttonList.setAdapter(buttonAdapter); - - buttonAdapter.registerAdapterDataObserver(new AttachmentButtonCenterHelper(buttonList)); mediaList.setLayoutManager(new GridLayoutManager(context, 1, GridLayoutManager.HORIZONTAL, false)); - buttonList.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)); - buttonAdapter.setButtons(DEFAULT_BUTTONS); + buttonList.setButtons(DEFAULT_BUTTONS); } public void setCallback(@NonNull Callback callback) { @@ -111,9 +104,9 @@ public void setCallback(@NonNull Callback callback) { public void filterAttachmentKeyboardButtons(@Nullable Predicate buttonPredicate) { if (buttonPredicate == null) { - buttonAdapter.setButtons(DEFAULT_BUTTONS); + buttonList.setButtons(DEFAULT_BUTTONS); } else { - buttonAdapter.setButtons(DEFAULT_BUTTONS.stream().filter(buttonPredicate).collect(Collectors.toList())); + buttonList.setButtons(DEFAULT_BUTTONS.stream().filter(buttonPredicate).collect(Collectors.toList())); } } @@ -163,7 +156,6 @@ public void setWallpaperEnabled(boolean wallpaperEnabled) { } else { container.setBackgroundColor(getContext().getResources().getColor(R.color.signal_background_primary)); } - buttonAdapter.setWallpaperEnabled(wallpaperEnabled); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java deleted file mode 100644 index 3ab4cd10dca..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; - -import org.thoughtcrime.securesms.R; - -import java.util.ArrayList; -import java.util.List; - -class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter { - - private final List buttons; - private final Listener listener; - - private boolean wallpaperEnabled; - - AttachmentKeyboardButtonAdapter(@NonNull Listener listener) { - this.buttons = new ArrayList<>(); - this.listener = listener; - - setHasStableIds(true); - } - - @Override - public long getItemId(int position) { - return buttons.get(position).getTitleRes(); - } - - @Override - public @NonNull - ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ButtonViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.attachment_keyboard_button_item, parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) { - holder.bind(buttons.get(position), wallpaperEnabled, listener); - } - - @Override - public void onViewRecycled(@NonNull ButtonViewHolder holder) { - holder.recycle(); - } - - @Override - public int getItemCount() { - return buttons.size(); - } - - public void setButtons(@NonNull List buttons) { - this.buttons.clear(); - this.buttons.addAll(buttons); - notifyDataSetChanged(); - } - - public void setWallpaperEnabled(boolean enabled) { - if (wallpaperEnabled != enabled) { - wallpaperEnabled = enabled; - notifyDataSetChanged(); - } - } - - interface Listener { - void onClick(@NonNull AttachmentKeyboardButton button); - } - - static class ButtonViewHolder extends RecyclerView.ViewHolder { - - private final ImageView image; - private final TextView title; - - public ButtonViewHolder(@NonNull View itemView) { - super(itemView); - - this.image = itemView.findViewById(R.id.icon); - this.title = itemView.findViewById(R.id.label); - } - - void bind(@NonNull AttachmentKeyboardButton button, boolean wallpaperEnabled, @NonNull Listener listener) { - image.setImageResource(button.getIconRes()); - title.setText(button.getTitleRes()); - - itemView.setOnClickListener(v -> listener.onClick(button)); - } - - void recycle() { - itemView.setOnClickListener(null); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt new file mode 100644 index 00000000000..5ce70470481 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.HorizontalScrollView +import android.widget.LinearLayout +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ButtonStripItemView +import java.util.function.Consumer + +class AttachmentKeyboardButtonList @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) : HorizontalScrollView(context, attrs, defStyleAttr) { + + private val inner: LinearLayout + private var onButtonClicked: Consumer? = null + + init { + inflate(context, R.layout.attachment_keyboard_button_list, this) + inner = findViewById(R.id.attachment_keyboard_button_list_inner_linearlayout) + } + + fun setButtons(newButtons: List) { + inner.removeAllViews() + newButtons.forEach { inner.addView(buttonToView(it)) } + AttachmentButtonCenterHelper.recenter(inner) + } + + private fun buttonToView(button: AttachmentKeyboardButton): View { + return (LayoutInflater.from(context).inflate(R.layout.attachment_keyboard_button_item, inner, false) as ButtonStripItemView).apply { + fillFromButton(button) + setOnClickListener { onButtonClicked?.accept(button) } + } + } + + /** Sets a callback that will be invoked when a button is clicked. */ + fun setOnButtonClickedCallback(callback: Consumer) { + onButtonClicked = callback + } + +} diff --git a/app/src/main/res/layout/attachment_keyboard.xml b/app/src/main/res/layout/attachment_keyboard.xml index 8fd1d230bf9..e4d038c8cd2 100644 --- a/app/src/main/res/layout/attachment_keyboard.xml +++ b/app/src/main/res/layout/attachment_keyboard.xml @@ -45,13 +45,11 @@ android:visibility="gone" app:icon="@drawable/symbol_settings_android_24" /> - diff --git a/app/src/main/res/layout/attachment_keyboard_button_list.xml b/app/src/main/res/layout/attachment_keyboard_button_list.xml new file mode 100644 index 00000000000..ab9f459fee4 --- /dev/null +++ b/app/src/main/res/layout/attachment_keyboard_button_list.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file From 923cee8dfa07236c36c0d17a1449d603ed03db83 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sat, 28 Jun 2025 05:28:01 -0700 Subject: [PATCH 04/23] Fade out the fake keyboard when closing it When we remove the fake keyboard fragment, use an animated transition to make it look less choppy. Also don't forcibly end the smooth transition for the fake keyboard unless the animation is actually in progress. This prevents erroneously setting the keyboard guideline to an obsolete value if it was moved by the built-in IME animation in between. --- .../components/InputAwareConstraintLayout.kt | 11 +++++++++ .../components/InsetAwareConstraintLayout.kt | 23 ++++++++++++------- .../AttachmentKeyboardButton.java | 1 + app/src/main/res/anim/fade_out_slowly.xml | 6 +++++ 4 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/anim/fade_out_slowly.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt index c9df126b89e..006444d6ed3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt @@ -10,6 +10,8 @@ import android.util.AttributeSet import android.widget.EditText import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.util.ViewUtil @@ -23,6 +25,10 @@ class InputAwareConstraintLayout @JvmOverloads constructor( defStyleAttr: Int = 0 ) : InsetAwareConstraintLayout(context, attrs, defStyleAttr) { + companion object { + private val TAG = Log.tag(InputAwareConstraintLayout::class) + } + private var inputId: Int? = null private var input: Fragment? = null private var wasKeyboardVisibleBeforeToggle: Boolean = false @@ -80,13 +86,16 @@ class InputAwareConstraintLayout @JvmOverloads constructor( } fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, showSoftKeyOnHide: Boolean = wasKeyboardVisibleBeforeToggle) { + Log.w(TAG, "yolo InputAwareConstraintLayout.toggleInput") if (fragmentCreator.id == inputId) { + Log.w(TAG, "yolo InputAwareConstraintLayout.toggleInput - fragmentCreator.id == ${fragmentCreator.id} == inputId") if (showSoftKeyOnHide) { showSoftkey(imeTarget) } else { hideInput(resetKeyboardGuideline = true) } } else { + Log.w(TAG, "yolo InputAwareConstraintLayout.toggleInput - the else case") wasKeyboardVisibleBeforeToggle = isKeyboardShowing hideInput(resetKeyboardGuideline = false) showInput(fragmentCreator, imeTarget) @@ -124,11 +133,13 @@ class InputAwareConstraintLayout @JvmOverloads constructor( } private fun hideInput(resetKeyboardGuideline: Boolean) { + Log.w(TAG, "yolo hideInput called ${input.toString()}, resetKeyboardGuideline=$resetKeyboardGuideline") val inputHidden = input != null input?.let { (input as? InputFragment)?.hide() fragmentManager .beginTransaction() + .setCustomAnimations(0, R.anim.fade_out_slowly) .remove(it) .commit() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index e175a364ea0..bf220b8d57f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -164,15 +164,20 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( if (!overridingKeyboard) { if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom) + Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (keyboardInsets): setting guideline=${keyboardInsets.bottom}") + //animateKeyboardGuidelineTo(windowInsets.bottom) } else { + Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (keyboardInsets/else): ${keyboardInsets.bottom}") keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom } } } else if (!overridingKeyboard) { if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(windowInsets.bottom) - // animateKeyboardGuidelineTo(windowInsets.bottom) + Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (windowInsets): setting guideline=${windowInsets.bottom}") + //animateKeyboardGuidelineTo(windowInsets.bottom) } else { + Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (windowInsets/else): ${windowInsets.bottom}") keyboardAnimator.endingGuidelineEnd = windowInsets.bottom } } @@ -198,6 +203,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun clearKeyboardGuidelineOverride() { overridingKeyboard = false + Log.w(TAG, "yolo InsetAwareConstraintLayout.clearKeyboardGuidelineOverride: ${keyboardGuideline.guidelineEnd}") } protected fun resetKeyboardGuideline() { @@ -207,19 +213,16 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } private fun animateKeyboardGuidelineTo(target: Int) { - if (keyboardGuideline != null) { + if (otherKeyboardAnimator?.isRunning == true) { otherKeyboardAnimator?.end() otherKeyboardAnimator = null } - otherKeyboardAnimator = ValueAnimator.ofInt(target, target, keyboardGuideline.guidelineEnd, target, keyboardGuideline.guidelineEnd, target).apply { - duration = 10_000 + Log.w(TAG, "yolo InsetAwareConstraintLayout.ValueAnimator: animating ${keyboardGuideline.guidelineEnd} -> $target") + otherKeyboardAnimator = ValueAnimator.ofInt(keyboardGuideline.guidelineEnd, target).apply { + duration = 2_500 addUpdateListener { animation -> (animation.animatedValue as? Int)?.let { currentValue -> keyboardGuideline?.setGuidelineEnd(currentValue) - forceLayout() - requestLayout() - invalidate() - forceLayout() } } start() @@ -305,6 +308,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( animating = true startingGuidelineEnd = keyboardGuideline.guidelineEnd + Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: animating from $startingGuidelineEnd") } override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat { @@ -324,8 +328,10 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( }.toInt() if (growing) { + Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: progress ${estimatedKeyboardHeight.coerceAtLeast(startingGuidelineEnd)}") keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(startingGuidelineEnd)) } else { + Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: progress ${estimatedKeyboardHeight.coerceAtLeast(endingGuidelineEnd)}") keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(endingGuidelineEnd)) } @@ -338,6 +344,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } keyboardGuideline?.setGuidelineEnd(endingGuidelineEnd) + Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: animated to $endingGuidelineEnd") animating = false } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java index 20812700f11..bc6181e727f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java @@ -15,6 +15,7 @@ public enum AttachmentKeyboardButton { private final int titleRes; private final int iconRes; + // TODO: add contentDescription to the icon resource for improved accessibility AttachmentKeyboardButton(@StringRes int titleRes, @DrawableRes int iconRes) { this.titleRes = titleRes; diff --git a/app/src/main/res/anim/fade_out_slowly.xml b/app/src/main/res/anim/fade_out_slowly.xml new file mode 100644 index 00000000000..5fd531f0741 --- /dev/null +++ b/app/src/main/res/anim/fade_out_slowly.xml @@ -0,0 +1,6 @@ + From 4489baa9b56470c4ebac373b24c61b061e5fe7fd Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sat, 28 Jun 2025 22:07:06 -0700 Subject: [PATCH 05/23] Fix error in merge conflict --- .../securesms/components/InsetAwareConstraintLayout.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index bf220b8d57f..7ec3c81125a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -209,6 +209,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun resetKeyboardGuideline() { clearKeyboardGuidelineOverride() // keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd) + keyboardAnimator.endingGuidelineEnd = navigationBarGuideline.guidelineEnd animateKeyboardGuidelineTo(navigationBarGuideline.guidelineEnd) } From 587457225af9b26a4d0f0d5757e1b4b603008b58 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sat, 28 Jun 2025 22:25:25 -0700 Subject: [PATCH 06/23] Don't let the keyboard guideline animators step on each other --- .../securesms/components/InsetAwareConstraintLayout.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index 7ec3c81125a..2aa5c854635 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -218,6 +218,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( otherKeyboardAnimator?.end() otherKeyboardAnimator = null } + if (keyboardAnimator.animating) { + // If Android is animating the keyboard in/out, forgo our own animation. + keyboardGuideline?.setGuidelineEnd(target) + return + } Log.w(TAG, "yolo InsetAwareConstraintLayout.ValueAnimator: animating ${keyboardGuideline.guidelineEnd} -> $target") otherKeyboardAnimator = ValueAnimator.ofInt(keyboardGuideline.guidelineEnd, target).apply { duration = 2_500 @@ -307,6 +312,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( return } + if (otherKeyboardAnimator?.isRunning == true) { + // Terminate other keyboard guideline animations if they're still running + otherKeyboardAnimator?.end() + otherKeyboardAnimator = null + } animating = true startingGuidelineEnd = keyboardGuideline.guidelineEnd Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: animating from $startingGuidelineEnd") From 833c09ae0a839068505974a4e44d9a225b9aaf73 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sun, 29 Jun 2025 00:13:08 -0700 Subject: [PATCH 07/23] Speed up the animations now that we're done debugging --- app/src/main/res/anim/fade_out_slowly.xml | 3 ++- app/src/main/res/values/integers.xml | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/anim/fade_out_slowly.xml b/app/src/main/res/anim/fade_out_slowly.xml index 5fd531f0741..ff7fbd2e1d2 100644 --- a/app/src/main/res/anim/fade_out_slowly.xml +++ b/app/src/main/res/anim/fade_out_slowly.xml @@ -1,6 +1,7 @@ + + android:duration="@integer/fake_keyboard_hide_duration" /> diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index bc3ee9a344e..a11eef3028e 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -9,4 +9,6 @@ 5000 150 - \ No newline at end of file + + 300 + From 51cf1f4ebf6e3ef656d0b59b1b8a0b1ab39aa349 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sun, 29 Jun 2025 00:14:13 -0700 Subject: [PATCH 08/23] Use new duration for animating the fake keyboard --- .../securesms/components/InsetAwareConstraintLayout.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index 2aa5c854635..38d15c0ae4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -225,7 +225,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } Log.w(TAG, "yolo InsetAwareConstraintLayout.ValueAnimator: animating ${keyboardGuideline.guidelineEnd} -> $target") otherKeyboardAnimator = ValueAnimator.ofInt(keyboardGuideline.guidelineEnd, target).apply { - duration = 2_500 + duration = resources.getInteger(R.integer.fake_keyboard_hide_duration).toLong() addUpdateListener { animation -> (animation.animatedValue as? Int)?.let { currentValue -> keyboardGuideline?.setGuidelineEnd(currentValue) From 26b9c8ba624ef7bec545ea4b35a57bf9812d9171 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sun, 29 Jun 2025 00:14:30 -0700 Subject: [PATCH 09/23] Clean up log statements - Keep some useful ones, delete some less useful ones - Convert levels from warn to debug --- .../components/InputAwareConstraintLayout.kt | 5 +---- .../components/InsetAwareConstraintLayout.kt | 19 ++++++++----------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt index 006444d6ed3..982897cf999 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt @@ -86,16 +86,13 @@ class InputAwareConstraintLayout @JvmOverloads constructor( } fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, showSoftKeyOnHide: Boolean = wasKeyboardVisibleBeforeToggle) { - Log.w(TAG, "yolo InputAwareConstraintLayout.toggleInput") if (fragmentCreator.id == inputId) { - Log.w(TAG, "yolo InputAwareConstraintLayout.toggleInput - fragmentCreator.id == ${fragmentCreator.id} == inputId") if (showSoftKeyOnHide) { showSoftkey(imeTarget) } else { hideInput(resetKeyboardGuideline = true) } } else { - Log.w(TAG, "yolo InputAwareConstraintLayout.toggleInput - the else case") wasKeyboardVisibleBeforeToggle = isKeyboardShowing hideInput(resetKeyboardGuideline = false) showInput(fragmentCreator, imeTarget) @@ -133,7 +130,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor( } private fun hideInput(resetKeyboardGuideline: Boolean) { - Log.w(TAG, "yolo hideInput called ${input.toString()}, resetKeyboardGuideline=$resetKeyboardGuideline") + Log.d(TAG, "hideInput: ${input.toString()}, resetKeyboardGuideline=$resetKeyboardGuideline") val inputHidden = input != null input?.let { (input as? InputFragment)?.hide() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index 38d15c0ae4d..098f7c6e7e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -164,20 +164,20 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( if (!overridingKeyboard) { if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom) - Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (keyboardInsets): setting guideline=${keyboardInsets.bottom}") + Log.d(TAG, "applyInsets (keyboardInsets): setting guideline=${keyboardInsets.bottom}") //animateKeyboardGuidelineTo(windowInsets.bottom) } else { - Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (keyboardInsets/else): ${keyboardInsets.bottom}") + Log.d(TAG, "applyInsets (keyboardInsets/else): ${keyboardInsets.bottom}") keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom } } } else if (!overridingKeyboard) { if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(windowInsets.bottom) - Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (windowInsets): setting guideline=${windowInsets.bottom}") + Log.d(TAG, "applyInsets (windowInsets): setting guideline=${windowInsets.bottom}") //animateKeyboardGuidelineTo(windowInsets.bottom) } else { - Log.w(TAG, "yolo InsetAwareConstraintLayout.applyInsets (windowInsets/else): ${windowInsets.bottom}") + Log.d(TAG, "applyInsets (windowInsets/else): ${windowInsets.bottom}") keyboardAnimator.endingGuidelineEnd = windowInsets.bottom } } @@ -203,7 +203,6 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun clearKeyboardGuidelineOverride() { overridingKeyboard = false - Log.w(TAG, "yolo InsetAwareConstraintLayout.clearKeyboardGuidelineOverride: ${keyboardGuideline.guidelineEnd}") } protected fun resetKeyboardGuideline() { @@ -223,7 +222,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( keyboardGuideline?.setGuidelineEnd(target) return } - Log.w(TAG, "yolo InsetAwareConstraintLayout.ValueAnimator: animating ${keyboardGuideline.guidelineEnd} -> $target") + Log.d(TAG, "Manually animating keyboard guideline: ${keyboardGuideline.guidelineEnd} -> $target") otherKeyboardAnimator = ValueAnimator.ofInt(keyboardGuideline.guidelineEnd, target).apply { duration = resources.getInteger(R.integer.fake_keyboard_hide_duration).toLong() addUpdateListener { animation -> @@ -313,13 +312,13 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } if (otherKeyboardAnimator?.isRunning == true) { - // Terminate other keyboard guideline animations if they're still running + // Terminate other keyboard guideline animations as to not interfere with this one otherKeyboardAnimator?.end() otherKeyboardAnimator = null } animating = true startingGuidelineEnd = keyboardGuideline.guidelineEnd - Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: animating from $startingGuidelineEnd") + Log.d(TAG, "KeyboardInsetAnimator animating from $startingGuidelineEnd") } override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat { @@ -339,10 +338,8 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( }.toInt() if (growing) { - Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: progress ${estimatedKeyboardHeight.coerceAtLeast(startingGuidelineEnd)}") keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(startingGuidelineEnd)) } else { - Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: progress ${estimatedKeyboardHeight.coerceAtLeast(endingGuidelineEnd)}") keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(endingGuidelineEnd)) } @@ -355,7 +352,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } keyboardGuideline?.setGuidelineEnd(endingGuidelineEnd) - Log.w(TAG, "yolo InsetAwareConstraintLayout.WindowInsetsAnimationCompat: animated to $endingGuidelineEnd") + Log.d(TAG, "KeyboardInsetAnimator animated to $endingGuidelineEnd") animating = false } } From c3633648fb7906c53640f33eb92ee3f9ca7ccc9e Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Mon, 30 Jun 2025 00:57:53 -0700 Subject: [PATCH 10/23] Add fancy animated effect to the attach button --- .../securesms/conversation/v2/ConversationFragment.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 2df5762a972..e1745105087 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest +import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.ActivityOptions import android.app.PendingIntent @@ -1023,8 +1024,12 @@ class ConversationFragment : sendEditButton.setOnClickListener { handleSendEditMessage() } - val attachListener = { _: View -> + val attachListener = { v: View -> container.toggleInput(AttachmentKeyboardFragmentCreator, composeText) + ObjectAnimator.ofFloat(v, "rotation", v.rotation, (v.rotation + 45) % 360).run { + setDuration(resources.getInteger(R.integer.fake_keyboard_hide_duration).toLong()) + start() + } } binding.conversationInputPanel.attachButton.setOnClickListener(attachListener) binding.conversationInputPanel.inlineAttachmentButton.setOnClickListener(attachListener) From 0a627c76effcded2b47997c4ea4d2a6779f6cc8e Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Mon, 30 Jun 2025 01:28:12 -0700 Subject: [PATCH 11/23] Never mind it didn't do what I thought it would --- .../securesms/conversation/v2/ConversationFragment.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index e1745105087..2df5762a972 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -6,7 +6,6 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest -import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.ActivityOptions import android.app.PendingIntent @@ -1024,12 +1023,8 @@ class ConversationFragment : sendEditButton.setOnClickListener { handleSendEditMessage() } - val attachListener = { v: View -> + val attachListener = { _: View -> container.toggleInput(AttachmentKeyboardFragmentCreator, composeText) - ObjectAnimator.ofFloat(v, "rotation", v.rotation, (v.rotation + 45) % 360).run { - setDuration(resources.getInteger(R.integer.fake_keyboard_hide_duration).toLong()) - start() - } } binding.conversationInputPanel.attachButton.setOnClickListener(attachListener) binding.conversationInputPanel.inlineAttachmentButton.setOnClickListener(attachListener) From 638855aaaf2d7af08e61c13ecdc082392a0c602a Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Mon, 30 Jun 2025 04:18:30 -0700 Subject: [PATCH 12/23] Animate the attach button when the attachment keyboard is opened/closed This makes things look a tad bit nicer, and mimics the behaviour of the keyboard pager toggle button. (The keyboard pager toggle button doesn't have an animation yet, but at least it changes its presentation depending on whether or not the keyboard pager is open or not). --- .../securesms/components/InputPanel.java | 35 +++++++++++++++++++ .../conversation/AttachmentKeyboard.java | 5 +++ .../conversation/v2/ConversationFragment.kt | 10 ++++++ .../v2/keyboard/AttachmentKeyboardFragment.kt | 12 ++++++- 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 49fdb808210..d0d0f22510f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java @@ -96,6 +96,8 @@ public class InputPanel extends ConstraintLayout private QuoteView quoteView; private LinkPreviewView linkPreview; private EmojiToggle mediaKeyboard; + private ImageButton inlineAttachButton; + private ImageButton mainAttachButton; private ComposeText composeText; private ImageButton quickCameraToggle; private ImageButton quickAudioToggle; @@ -112,6 +114,7 @@ public class InputPanel extends ConstraintLayout private MicrophoneRecorderView microphoneRecorderView; private SlideToCancel slideToCancel; private RecordTime recordTime; + private ValueAnimator attachButtonAnimator; private ValueAnimator quoteAnimator; private ValueAnimator editMessageAnimator; private VoiceNoteDraftView voiceNoteDraftView; @@ -151,6 +154,8 @@ public void onFinishInflate() { this.quoteView = findViewById(R.id.quote_view); this.linkPreview = findViewById(R.id.link_preview); this.mediaKeyboard = findViewById(R.id.emoji_toggle); + this.inlineAttachButton = findViewById(R.id.inline_attachment_button); + this.mainAttachButton = findViewById(R.id.attach_button); this.composeText = findViewById(R.id.embedded_text_editor); this.composeTextContainer = findViewById(R.id.embedded_text_editor_container); this.quickCameraToggle = findViewById(R.id.quick_camera_toggle); @@ -380,6 +385,36 @@ public MediaKeyboard.MediaKeyboardListener getMediaKeyboardListener() { return mediaKeyboard; } + /** Sets the attach buttons to a + sign. */ + public void rotateAttachButtonsToOpen() { + if (attachButtonAnimator != null && attachButtonAnimator.isRunning()) { + attachButtonAnimator.end(); + } + attachButtonAnimator = createAttachmentButtonRotationAnimator(45, 0); + attachButtonAnimator.start(); + } + + /** Sets the attach buttons to a x sign. */ + public void rotateAttachButtonsToClose() { + if (attachButtonAnimator != null && attachButtonAnimator.isRunning()) { + attachButtonAnimator.end(); + } + attachButtonAnimator = createAttachmentButtonRotationAnimator(0, 45); + attachButtonAnimator.start(); + } + + private ValueAnimator createAttachmentButtonRotationAnimator(float start, float end) { + ValueAnimator animator = ValueAnimator.ofFloat(start, end).setDuration(getResources().getInteger(R.integer.fake_keyboard_hide_duration)); + animator.addUpdateListener( + animation -> { + float degrees = (float) animation.getAnimatedValue(); + inlineAttachButton.setRotation(degrees); + mainAttachButton.setRotation(degrees); + } + ); + return animator; + } + public void setWallpaperEnabled(boolean enabled) { final int iconTint; final int textColor; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java index 30e690eea40..56be0480c40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java @@ -254,4 +254,9 @@ public interface Callback { void onAttachmentPermissionsRequested(); void onDisplayMoreContextMenu(View v, boolean showAbove, boolean showAtStart); } + + public interface AttachmentKeyboardListener { + void onAttachmentKeyboardShown(); + void onAttachmentKeyboardHidden(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 2df5762a972..c2d2f5f0d41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -149,6 +149,7 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey.RecipientSearc import org.thoughtcrime.securesms.contactshare.Contact import org.thoughtcrime.securesms.contactshare.ContactUtil import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity +import org.thoughtcrime.securesms.conversation.AttachmentKeyboard import org.thoughtcrime.securesms.conversation.AttachmentKeyboardButton import org.thoughtcrime.securesms.conversation.BadDecryptLearnMoreDialog import org.thoughtcrime.securesms.conversation.ConversationAdapter @@ -367,6 +368,7 @@ class ConversationFragment : StickerEventListener, StickerKeyboardPageFragment.Callback, MediaKeyboard.MediaKeyboardListener, + AttachmentKeyboard.AttachmentKeyboardListener, EmojiSearchFragment.Callback, ScheduleMessageTimePickerBottomSheet.ScheduleCallback, ScheduleMessageDialogCallback, @@ -866,6 +868,14 @@ class ConversationFragment : closeEmojiSearch() } + override fun onAttachmentKeyboardShown() { + inputPanel.rotateAttachButtonsToClose() + } + + override fun onAttachmentKeyboardHidden() { + inputPanel.rotateAttachButtonsToOpen() + } + override fun onKeyboardChanged(page: KeyboardPage) { inputPanel.mediaKeyboardListener.onKeyboardChanged(page) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt index 1215ceac75d..c941a9a8ec2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt @@ -18,6 +18,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.addTo import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.InputAwareConstraintLayout import org.thoughtcrime.securesms.conversation.AttachmentKeyboard import org.thoughtcrime.securesms.conversation.AttachmentKeyboardButton import org.thoughtcrime.securesms.conversation.ManageContextMenu @@ -27,13 +28,14 @@ import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.fragments.findListener import java.util.function.Predicate /** * Fragment wrapped version of [AttachmentKeyboard] to help encapsulate logic the view * needs from external sources. */ -class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_fragment), AttachmentKeyboard.Callback { +class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_fragment), AttachmentKeyboard.Callback, InputAwareConstraintLayout.InputFragment { companion object { const val RESULT_KEY = "AttachmentKeyboardFragmentResult" @@ -138,4 +140,12 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_ attachmentKeyboardView.filterAttachmentKeyboardButtons(removePaymentFilter) } } + + override fun show() { + findListener()?.onAttachmentKeyboardShown(); + } + + override fun hide() { + findListener()?.onAttachmentKeyboardHidden(); + } } From 90b22299425ba3a00694a628c73512cdb5657b93 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Mon, 30 Jun 2025 04:40:35 -0700 Subject: [PATCH 13/23] Remove files used solely for debugging purposes --- ...ttachment_keyboard_nestedlinearlayouts.xml | 84 ---------------- .../attachment_keyboard_relativelayout.xml | 97 ------------------- 2 files changed, 181 deletions(-) delete mode 100644 app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml delete mode 100644 app/src/main/res/layout/attachment_keyboard_relativelayout.xml diff --git a/app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml b/app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml deleted file mode 100644 index 5fbc1891e58..00000000000 --- a/app/src/main/res/layout/attachment_keyboard_nestedlinearlayouts.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/attachment_keyboard_relativelayout.xml b/app/src/main/res/layout/attachment_keyboard_relativelayout.xml deleted file mode 100644 index 0908a6cd53f..00000000000 --- a/app/src/main/res/layout/attachment_keyboard_relativelayout.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file From e64c998225427c6748b5ff3f6691bc1c819ccae2 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Thu, 25 Sep 2025 02:39:08 -0700 Subject: [PATCH 14/23] Make Kotlin things more idiomatic --- .../securesms/conversation/AttachmentKeyboard.java | 2 +- .../conversation/AttachmentKeyboardButtonList.kt | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java index 56be0480c40..25a3b16b48a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java @@ -75,7 +75,7 @@ private void init(@NonNull Context context) { }); buttonList = findViewById(R.id.attachment_keyboard_button_list); - buttonList.setOnButtonClickedCallback(button -> { + buttonList.setOnButtonClicked(button -> { if (callback != null) { callback.onAttachmentSelectorClicked(button); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt index 5ce70470481..45c13d0db09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -21,7 +21,7 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( defStyleAttr: Int = 0) : HorizontalScrollView(context, attrs, defStyleAttr) { private val inner: LinearLayout - private var onButtonClicked: Consumer? = null + var onButtonClicked: Consumer = Consumer { _ -> } init { inflate(context, R.layout.attachment_keyboard_button_list, this) @@ -30,20 +30,14 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( fun setButtons(newButtons: List) { inner.removeAllViews() - newButtons.forEach { inner.addView(buttonToView(it)) } + newButtons.asSequence().map { buttonToView(it) }.forEach { inner.addView(it) } AttachmentButtonCenterHelper.recenter(inner) } private fun buttonToView(button: AttachmentKeyboardButton): View { return (LayoutInflater.from(context).inflate(R.layout.attachment_keyboard_button_item, inner, false) as ButtonStripItemView).apply { fillFromButton(button) - setOnClickListener { onButtonClicked?.accept(button) } + setOnClickListener { onButtonClicked.accept(button) } } } - - /** Sets a callback that will be invoked when a button is clicked. */ - fun setOnButtonClickedCallback(callback: Consumer) { - onButtonClicked = callback - } - } From be62462b6fd290b20790829aa1c9021e2b00bca6 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Fri, 26 Sep 2025 18:21:08 -0700 Subject: [PATCH 15/23] Fix recentering algorithm - Make recentering algorithm more concise - Make Kotlin code more idiomatic - Remove hard-coded values from recentering algorithm - Recenter when the buttons' container changes too, not just when the number of buttons changes. This fixes a bug where the buttons would not be centered after changing the device orientation while the buttons are open. --- .../AttachmentButtonCenterHelper.kt | 36 ++++++++++--------- .../AttachmentKeyboardButtonList.kt | 22 ++++++++++-- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt index 8787620e694..6a4b3658748 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt @@ -1,31 +1,33 @@ package org.thoughtcrime.securesms.conversation -import android.view.ViewGroup -import androidx.core.view.doOnNextLayout +import android.view.View +import androidx.core.view.doOnLayout import org.signal.core.util.DimensionUnit +import org.signal.core.util.logging.Log /** * Adds necessary padding to each side of the given ViewGroup in order to ensure that - * if all buttons can fit in the visible real-estate on screen, they are centered. + * if all buttons can fit in the visible real-estate as defined by the wrapper view, + * then they are centered. However if there are too many buttons to fit on the screen, + * then put a basic amount of padding on each side so that it looks nice when scrolling + * to either end. */ object AttachmentButtonCenterHelper { - private val itemWidth: Float = DimensionUnit.DP.toPixels(88f) - private val defaultPadding: Float = DimensionUnit.DP.toPixels(16f) + private val DEFAULT_PADDING = DimensionUnit.DP.toPixels(16f).toInt() - fun recenter(container: ViewGroup) { - val itemCount = container.childCount - val requiredSpace = itemWidth * itemCount - - container.doOnNextLayout { - if (it.measuredWidth >= requiredSpace) { - val extraSpace = it.measuredWidth - requiredSpace - val availablePadding = extraSpace / 2f - it.post { - it.setPadding(availablePadding.toInt(), it.paddingTop, availablePadding.toInt(), it.paddingBottom) + fun recenter(buttonHolder: View, wrapper: View) { + buttonHolder.let { + it.post { + it.setPadding(DEFAULT_PADDING, it.paddingTop, DEFAULT_PADDING, it.paddingBottom) + } + it.post { + val extraSpace = wrapper.measuredWidth - it.measuredWidth + val horizontalPadding = when { + extraSpace > 0 -> (extraSpace / 2f).toInt() + else -> DEFAULT_PADDING } - } else { - it.setPadding(defaultPadding.toInt(), it.paddingTop, defaultPadding.toInt(), it.paddingBottom) + it.setPadding(horizontalPadding, it.paddingTop, horizontalPadding, it.paddingBottom) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt index 45c13d0db09..ea573a80d5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -20,22 +20,40 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0) : HorizontalScrollView(context, attrs, defStyleAttr) { + private val inflater = LayoutInflater.from(context) private val inner: LinearLayout + private val recenterIfDimensionsChange: OnLayoutChangeListener var onButtonClicked: Consumer = Consumer { _ -> } init { inflate(context, R.layout.attachment_keyboard_button_list, this) inner = findViewById(R.id.attachment_keyboard_button_list_inner_linearlayout) + recenterIfDimensionsChange = OnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ -> + if (oldLeft == left && oldRight == right) + return@OnLayoutChangeListener + AttachmentButtonCenterHelper.recenter(inner, this) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + addOnLayoutChangeListener(recenterIfDimensionsChange) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + removeOnLayoutChangeListener(recenterIfDimensionsChange) } fun setButtons(newButtons: List) { inner.removeAllViews() newButtons.asSequence().map { buttonToView(it) }.forEach { inner.addView(it) } - AttachmentButtonCenterHelper.recenter(inner) + AttachmentButtonCenterHelper.recenter(inner, this) } private fun buttonToView(button: AttachmentKeyboardButton): View { - return (LayoutInflater.from(context).inflate(R.layout.attachment_keyboard_button_item, inner, false) as ButtonStripItemView).apply { + val buttonView = inflater.inflate(R.layout.attachment_keyboard_button_item, inner, false) as ButtonStripItemView + return buttonView.apply { fillFromButton(button) setOnClickListener { onButtonClicked.accept(button) } } From b346a2188a06b357083cee525dc87b7b768e762c Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sat, 27 Sep 2025 03:27:35 -0700 Subject: [PATCH 16/23] Shorten fake keyboard show/hide animation to 200ms 300 is just a tad too long compared to the Android OS actual duration for the IME show/hide animation. --- app/src/main/res/values/integers.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index a11eef3028e..2efb4215573 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -10,5 +10,5 @@ 150 - 300 + 200 From 98c463e695259ac1ad77ff3575727cd4630a47e7 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sat, 27 Sep 2025 04:32:15 -0700 Subject: [PATCH 17/23] Import extensions for more idiomatic Kotlin --- .../securesms/conversation/AttachmentKeyboardButtonList.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt index ea573a80d5d..eff5d0c3660 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -11,6 +11,7 @@ import android.view.LayoutInflater import android.view.View import android.widget.HorizontalScrollView import android.widget.LinearLayout +import androidx.core.view.plusAssign import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ButtonStripItemView import java.util.function.Consumer @@ -47,7 +48,7 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( fun setButtons(newButtons: List) { inner.removeAllViews() - newButtons.asSequence().map { buttonToView(it) }.forEach { inner.addView(it) } + newButtons.forEach { inner += buttonToView(it) } AttachmentButtonCenterHelper.recenter(inner, this) } From 24465d2df7915d62a77fe6e8fe350fa96a87841f Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Sun, 28 Sep 2025 03:43:37 -0700 Subject: [PATCH 18/23] Hopefully fix things It worked on my emulator but not my physical device :( Maybe it's because my emulator is slower than my physical device? Either way, I hope this fixes things... - Simplify recentering algorithm - Do not re-layout the attachment keyboard buttons if the app tries to set the same set of buttons - Make things more readable - Make Kotlin code more idiomatic - Add logging - Post recentering requests to hopefully fix it on my physical device - Simplify call hierarchy for recentering Request a recenter when the wrapper's width changes or when we add/remove buttons from the button list. If you think about it, nothing else is required. --- .../AttachmentButtonCenterHelper.kt | 24 +++++++--------- .../AttachmentKeyboardButtonList.kt | 28 +++++++++++++------ .../attachment_keyboard_button_list.xml | 2 +- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt index 6a4b3658748..5fea377c0b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.conversation import android.view.View -import androidx.core.view.doOnLayout import org.signal.core.util.DimensionUnit import org.signal.core.util.logging.Log @@ -14,21 +13,18 @@ import org.signal.core.util.logging.Log */ object AttachmentButtonCenterHelper { + private val TAG = Log.tag(AttachmentButtonCenterHelper::class) private val DEFAULT_PADDING = DimensionUnit.DP.toPixels(16f).toInt() fun recenter(buttonHolder: View, wrapper: View) { - buttonHolder.let { - it.post { - it.setPadding(DEFAULT_PADDING, it.paddingTop, DEFAULT_PADDING, it.paddingBottom) - } - it.post { - val extraSpace = wrapper.measuredWidth - it.measuredWidth - val horizontalPadding = when { - extraSpace > 0 -> (extraSpace / 2f).toInt() - else -> DEFAULT_PADDING - } - it.setPadding(horizontalPadding, it.paddingTop, horizontalPadding, it.paddingBottom) - } - } + // The core width of the list of buttons not include any padding. + val buttonHolderCoreWidth = buttonHolder.run { width - (paddingLeft + paddingRight) } + val extraSpace = wrapper.width - buttonHolderCoreWidth + val horizontalPadding = if (extraSpace >= 0) + (extraSpace / 2f).toInt() + else + DEFAULT_PADDING + Log.i(TAG, "will add $horizontalPadding px on either side") + buttonHolder.apply { setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt index eff5d0c3660..ce79bba109c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -8,12 +8,12 @@ package org.thoughtcrime.securesms.conversation import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater -import android.view.View import android.widget.HorizontalScrollView import android.widget.LinearLayout import androidx.core.view.plusAssign import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ButtonStripItemView +import org.signal.core.util.logging.Log import java.util.function.Consumer class AttachmentKeyboardButtonList @JvmOverloads constructor( @@ -21,16 +21,22 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0) : HorizontalScrollView(context, attrs, defStyleAttr) { + companion object { + val TAG = Log.tag(AttachmentKeyboardButtonList::class) + } + private val inflater = LayoutInflater.from(context) private val inner: LinearLayout - private val recenterIfDimensionsChange: OnLayoutChangeListener + private val recenterOnWidthChange: OnLayoutChangeListener var onButtonClicked: Consumer = Consumer { _ -> } + private var currentButtons: List = listOf() + init { inflate(context, R.layout.attachment_keyboard_button_list, this) inner = findViewById(R.id.attachment_keyboard_button_list_inner_linearlayout) - recenterIfDimensionsChange = OnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ -> - if (oldLeft == left && oldRight == right) + recenterOnWidthChange = OnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ -> + if (oldRight - oldLeft == right - left) return@OnLayoutChangeListener AttachmentButtonCenterHelper.recenter(inner, this) } @@ -38,21 +44,25 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - addOnLayoutChangeListener(recenterIfDimensionsChange) + addOnLayoutChangeListener(recenterOnWidthChange) } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - removeOnLayoutChangeListener(recenterIfDimensionsChange) + removeOnLayoutChangeListener(recenterOnWidthChange) } fun setButtons(newButtons: List) { + if (currentButtons == newButtons) + return + Log.i(TAG, "setButtons: $currentButtons -> $newButtons") + currentButtons = newButtons inner.removeAllViews() - newButtons.forEach { inner += buttonToView(it) } - AttachmentButtonCenterHelper.recenter(inner, this) + newButtons.forEach { inner += inflateButton(it) } + inner.post { AttachmentButtonCenterHelper.recenter(inner, this) } } - private fun buttonToView(button: AttachmentKeyboardButton): View { + private fun inflateButton(button: AttachmentKeyboardButton): ButtonStripItemView { val buttonView = inflater.inflate(R.layout.attachment_keyboard_button_item, inner, false) as ButtonStripItemView return buttonView.apply { fillFromButton(button) diff --git a/app/src/main/res/layout/attachment_keyboard_button_list.xml b/app/src/main/res/layout/attachment_keyboard_button_list.xml index ab9f459fee4..b2864a93d18 100644 --- a/app/src/main/res/layout/attachment_keyboard_button_list.xml +++ b/app/src/main/res/layout/attachment_keyboard_button_list.xml @@ -10,4 +10,4 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" /> - \ No newline at end of file + From 95b61b0cf7f097861ff5d41f89eac7f922b0561b Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Mon, 29 Sep 2025 02:03:17 -0700 Subject: [PATCH 19/23] Run ./gradlew format, remove dev comments, lower debug level for logs --- .../securesms/components/InputAwareConstraintLayout.kt | 3 +-- .../securesms/components/InsetAwareConstraintLayout.kt | 4 ---- .../securesms/conversation/AttachmentButtonCenterHelper.kt | 2 +- .../securesms/conversation/AttachmentKeyboardButtonList.kt | 7 ++++--- .../conversation/v2/keyboard/AttachmentKeyboardFragment.kt | 4 ++-- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt index 982897cf999..93f632c3744 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt @@ -10,7 +10,6 @@ import android.util.AttributeSet import android.widget.EditText import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentTransaction import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.util.ViewUtil @@ -130,7 +129,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor( } private fun hideInput(resetKeyboardGuideline: Boolean) { - Log.d(TAG, "hideInput: ${input.toString()}, resetKeyboardGuideline=$resetKeyboardGuideline") + Log.d(TAG, "hideInput: $input, resetKeyboardGuideline=$resetKeyboardGuideline") val inputHidden = input != null input?.let { (input as? InputFragment)?.hide() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index abb5b250d65..dcff8499b0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -192,7 +192,6 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom) Log.d(TAG, "applyInsets (keyboardInsets): setting guideline=${keyboardInsets.bottom}") - //animateKeyboardGuidelineTo(windowInsets.bottom) } else { Log.d(TAG, "applyInsets (keyboardInsets/else): ${keyboardInsets.bottom}") keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom @@ -202,7 +201,6 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(windowInsets.bottom) Log.d(TAG, "applyInsets (windowInsets): setting guideline=${windowInsets.bottom}") - //animateKeyboardGuidelineTo(windowInsets.bottom) } else { Log.d(TAG, "applyInsets (windowInsets/else): ${windowInsets.bottom}") keyboardAnimator.endingGuidelineEnd = windowInsets.bottom @@ -224,7 +222,6 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun overrideKeyboardGuidelineWithPreviousHeight() { overridingKeyboard = true - // keyboardGuideline?.setGuidelineEnd(getKeyboardHeight()) animateKeyboardGuidelineTo(getKeyboardHeight()) } @@ -234,7 +231,6 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun resetKeyboardGuideline() { clearKeyboardGuidelineOverride() - // keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd) keyboardAnimator.endingGuidelineEnd = navigationBarGuideline.guidelineEnd animateKeyboardGuidelineTo(navigationBarGuideline.guidelineEnd) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt index 5fea377c0b8..890698063c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt @@ -24,7 +24,7 @@ object AttachmentButtonCenterHelper { (extraSpace / 2f).toInt() else DEFAULT_PADDING - Log.i(TAG, "will add $horizontalPadding px on either side") + Log.d(TAG, "will add $horizontalPadding px on either side") buttonHolder.apply { setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt index ce79bba109c..5927f8fd96f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -11,15 +11,16 @@ import android.view.LayoutInflater import android.widget.HorizontalScrollView import android.widget.LinearLayout import androidx.core.view.plusAssign +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ButtonStripItemView -import org.signal.core.util.logging.Log import java.util.function.Consumer class AttachmentKeyboardButtonList @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) : HorizontalScrollView(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0 +) : HorizontalScrollView(context, attrs, defStyleAttr) { companion object { val TAG = Log.tag(AttachmentKeyboardButtonList::class) @@ -55,7 +56,7 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( fun setButtons(newButtons: List) { if (currentButtons == newButtons) return - Log.i(TAG, "setButtons: $currentButtons -> $newButtons") + Log.d(TAG, "setButtons: $currentButtons -> $newButtons") currentButtons = newButtons inner.removeAllViews() newButtons.forEach { inner += inflateButton(it) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt index c941a9a8ec2..3989c814b0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt @@ -142,10 +142,10 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_ } override fun show() { - findListener()?.onAttachmentKeyboardShown(); + findListener()?.onAttachmentKeyboardShown() } override fun hide() { - findListener()?.onAttachmentKeyboardHidden(); + findListener()?.onAttachmentKeyboardHidden() } } From 9528f518f04830dc38daafdcff5487ebf88781ac Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Thu, 27 Nov 2025 02:50:13 -0800 Subject: [PATCH 20/23] Fix weird attachment button recenterer regression Story time - as far as I can remember, the width attribute included padding, so calculating the core width required doing val coreWidth = myView.run { width - (paddingLeft + paddingRight) } to account for that. This is weird if you're used to CSS. However something changed between 7.58 and 7.66, but the width attribute no longer includes padding at all, which mirrors the behaviour of CSS. So, yay, I guess? To be clear, I have no clue why this behaviour changed. My current suspects are Signal (doubt), Android Studio (maybe), and Google silently changing upstream dependencies (maybe). --- .../conversation/AttachmentButtonCenterHelper.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt index 890698063c2..3e64b40de80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt @@ -18,8 +18,14 @@ object AttachmentButtonCenterHelper { fun recenter(buttonHolder: View, wrapper: View) { // The core width of the list of buttons not include any padding. - val buttonHolderCoreWidth = buttonHolder.run { width - (paddingLeft + paddingRight) } - val extraSpace = wrapper.width - buttonHolderCoreWidth + // Story time - as far as I can remember, the width attribute included padding, so calculating + // the core width required doing myView.run { width - (paddingLeft + paddingRight) } to account + // for that. This is weird if you're used to CSS. + // However something changed between 7.58 and 7.66, but the width attribute no longer includes + // padding at all, which mirrors the behaviour of CSS. So, yay, I guess? + // To be clear, I have no clue why this behaviour changed. My current suspects are Signal (doubt), + // Android Studio (maybe), and Google silently changing upstream dependencies (maybe). + val extraSpace = wrapper.width - buttonHolder.width val horizontalPadding = if (extraSpace >= 0) (extraSpace / 2f).toInt() else From 8913eaad0460803697bb891b2bbab4f1526c5d34 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Thu, 27 Nov 2025 03:06:57 -0800 Subject: [PATCH 21/23] Shorten fake keyboard hide duration to 160ms The real keyboard hide duration has a real value: 160 milliseconds. [1] https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java;l=740;drc=be09f29954b93ee30644b45d90d539bdb78d4117 --- app/src/main/res/values/integers.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index 2efb4215573..f85ea312bf9 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -10,5 +10,6 @@ 150 - 200 + + 160 From f0d0750333ae40e34b0986ac19d5b5be50d0610d Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Fri, 28 Nov 2025 21:28:29 -0800 Subject: [PATCH 22/23] Fix the fix to the recentering algorithm Just kidding. It was actually a race condition where the padding was updated but that update was not propagated to the width/height yet, because layout hadn't occurred yet. To be clear, the width of a View in Android DOES include the padding. --- .../conversation/AttachmentButtonCenterHelper.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt index 3e64b40de80..39997285af6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt @@ -16,16 +16,12 @@ object AttachmentButtonCenterHelper { private val TAG = Log.tag(AttachmentButtonCenterHelper::class) private val DEFAULT_PADDING = DimensionUnit.DP.toPixels(16f).toInt() + @Synchronized fun recenter(buttonHolder: View, wrapper: View) { - // The core width of the list of buttons not include any padding. - // Story time - as far as I can remember, the width attribute included padding, so calculating - // the core width required doing myView.run { width - (paddingLeft + paddingRight) } to account - // for that. This is weird if you're used to CSS. - // However something changed between 7.58 and 7.66, but the width attribute no longer includes - // padding at all, which mirrors the behaviour of CSS. So, yay, I guess? - // To be clear, I have no clue why this behaviour changed. My current suspects are Signal (doubt), - // Android Studio (maybe), and Google silently changing upstream dependencies (maybe). - val extraSpace = wrapper.width - buttonHolder.width + buttonHolder.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + buttonHolder.layout(0, 0, buttonHolder.measuredWidth, buttonHolder.measuredHeight) + val buttonHolderCoreWidth = buttonHolder.run { width - (paddingLeft + paddingRight) } + val extraSpace = wrapper.width - buttonHolderCoreWidth val horizontalPadding = if (extraSpace >= 0) (extraSpace / 2f).toInt() else From 347601e5f62d2393e3fe9dcd25850e63176799d9 Mon Sep 17 00:00:00 2001 From: farewelltospring Date: Mon, 1 Dec 2025 04:35:18 -0800 Subject: [PATCH 23/23] Fix the fix to the fix to the recentering algorithm I had a big brain realisation to use the power of RxJava to make things work in a clean way that is probably more technically correct than manually invoking measure and layout. --- .../AttachmentButtonCenterHelper.kt | 61 ++++++++++++++++--- .../AttachmentKeyboardButtonList.kt | 14 ++--- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt index 39997285af6..cd41f5872d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentButtonCenterHelper.kt @@ -1,6 +1,11 @@ package org.thoughtcrime.securesms.conversation import android.view.View +import android.view.View.OnLayoutChangeListener +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.subjects.PublishSubject import org.signal.core.util.DimensionUnit import org.signal.core.util.logging.Log @@ -11,17 +16,55 @@ import org.signal.core.util.logging.Log * then put a basic amount of padding on each side so that it looks nice when scrolling * to either end. */ -object AttachmentButtonCenterHelper { +class AttachmentButtonCenterHelper(val buttonHolder: View, val wrapper: View) { - private val TAG = Log.tag(AttachmentButtonCenterHelper::class) - private val DEFAULT_PADDING = DimensionUnit.DP.toPixels(16f).toInt() + companion object { + val TAG = Log.tag(AttachmentButtonCenterHelper::class) + private val DEFAULT_PADDING = DimensionUnit.DP.toPixels(16f).toInt() + } + + /** The wrapper width is the maximum size of the button holder before scrollbars appear. */ + private val wrapperWidthObservable: PublishSubject = PublishSubject.create() + private val emitNewWrapperWidth = OnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ -> + if (oldRight - oldLeft == right - left) + return@OnLayoutChangeListener + wrapperWidthObservable.onNext(right - left) + } + + /** The "core width" of the button holder is the size of its contents. */ + private val coreWidthObservable: PublishSubject = PublishSubject.create() + private val emitNewCoreWidth = OnLayoutChangeListener { view, _, _, _, _, _, _, _, _ -> + val newCoreWidth = view.run { width - (paddingLeft + paddingRight) } + coreWidthObservable.onNext(newCoreWidth) + } + + private var listener: Disposable? = null + + fun attach() { + wrapper.addOnLayoutChangeListener(emitNewWrapperWidth) + buttonHolder.addOnLayoutChangeListener(emitNewCoreWidth) + + listener?.dispose() + listener = Observable.combineLatest(wrapperWidthObservable, coreWidthObservable, ::Pair) + .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { widths -> + val wrapperWidth = widths.first + val coreWidth = widths.second + Log.d(TAG, "wrapperWidth: $wrapperWidth, coreWidth: $coreWidth") + recenter(coreWidth, wrapperWidth) + } + } + + fun detach() { + wrapper.removeOnLayoutChangeListener(emitNewWrapperWidth) + buttonHolder.removeOnLayoutChangeListener(emitNewCoreWidth) + listener?.dispose() + listener = null + } - @Synchronized - fun recenter(buttonHolder: View, wrapper: View) { - buttonHolder.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) - buttonHolder.layout(0, 0, buttonHolder.measuredWidth, buttonHolder.measuredHeight) - val buttonHolderCoreWidth = buttonHolder.run { width - (paddingLeft + paddingRight) } - val extraSpace = wrapper.width - buttonHolderCoreWidth + fun recenter(buttonHolderCoreWidth: Int, wrapperWidth: Int) { + val extraSpace = wrapperWidth - buttonHolderCoreWidth val horizontalPadding = if (extraSpace >= 0) (extraSpace / 2f).toInt() else diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt index 5927f8fd96f..05b6a027b3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -28,7 +28,8 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( private val inflater = LayoutInflater.from(context) private val inner: LinearLayout - private val recenterOnWidthChange: OnLayoutChangeListener + + private val recenterHelper: AttachmentButtonCenterHelper var onButtonClicked: Consumer = Consumer { _ -> } private var currentButtons: List = listOf() @@ -36,21 +37,17 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( init { inflate(context, R.layout.attachment_keyboard_button_list, this) inner = findViewById(R.id.attachment_keyboard_button_list_inner_linearlayout) - recenterOnWidthChange = OnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ -> - if (oldRight - oldLeft == right - left) - return@OnLayoutChangeListener - AttachmentButtonCenterHelper.recenter(inner, this) - } + recenterHelper = AttachmentButtonCenterHelper(inner, this) } override fun onAttachedToWindow() { super.onAttachedToWindow() - addOnLayoutChangeListener(recenterOnWidthChange) + recenterHelper.attach() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - removeOnLayoutChangeListener(recenterOnWidthChange) + recenterHelper.detach() } fun setButtons(newButtons: List) { @@ -60,7 +57,6 @@ class AttachmentKeyboardButtonList @JvmOverloads constructor( currentButtons = newButtons inner.removeAllViews() newButtons.forEach { inner += inflateButton(it) } - inner.post { AttachmentButtonCenterHelper.recenter(inner, this) } } private fun inflateButton(button: AttachmentKeyboardButton): ButtonStripItemView {