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/components/InputAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt index c5757ee3f92..d8ef42d3291 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,7 @@ import android.util.AttributeSet import android.widget.EditText import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.util.ViewUtil @@ -23,6 +24,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 @@ -124,11 +129,13 @@ class InputAwareConstraintLayout @JvmOverloads constructor( } private fun hideInput(resetKeyboardGuideline: Boolean) { + Log.d(TAG, "hideInput: $input, 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/InputPanel.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 2da4700a358..740ff1f3410 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/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index 5af9f08f38f..31188e02ca8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.components +import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration import android.util.AttributeSet +import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.Guideline import androidx.core.content.withStyledAttributes @@ -58,6 +60,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( private val keyboardStateListeners: MutableSet = mutableSetOf() private val keyboardAnimator = KeyboardInsetAnimator() private var overridingKeyboard: Boolean = false + private var otherKeyboardAnimator: ValueAnimator? = null private var previousKeyboardHeight: Int = 0 private var previousStatusBarInset: Int = 0 @@ -157,14 +160,18 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( if (!overridingKeyboard) { if (!keyboardAnimator.animating) { keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom) + Log.d(TAG, "applyInsets (keyboardInsets): setting guideline=${keyboardInsets.bottom}") } else { + Log.d(TAG, "applyInsets (keyboardInsets/else): ${keyboardInsets.bottom}") keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom } } } else if (!overridingKeyboard) { if (!keyboardAnimator.animating) { + Log.d(TAG, "applyInsets (navigationBar): setting guideline=${navigationBar}") keyboardGuideline?.setGuidelineEnd(navigationBar) } else { + Log.d(TAG, "applyInsets (navigationBar/else): ${navigationBar}") keyboardAnimator.endingGuidelineEnd = navigationBar } } @@ -184,7 +191,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun overrideKeyboardGuidelineWithPreviousHeight() { overridingKeyboard = true - keyboardGuideline?.setGuidelineEnd(getKeyboardHeight()) + animateKeyboardGuidelineTo(getKeyboardHeight()) } protected fun clearKeyboardGuidelineOverride() { @@ -193,8 +200,30 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( protected fun resetKeyboardGuideline() { clearKeyboardGuidelineOverride() - keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd) keyboardAnimator.endingGuidelineEnd = navigationBarGuideline.guidelineEnd + animateKeyboardGuidelineTo(navigationBarGuideline.guidelineEnd) + } + + private fun animateKeyboardGuidelineTo(target: Int) { + if (otherKeyboardAnimator?.isRunning == true) { + otherKeyboardAnimator?.end() + otherKeyboardAnimator = null + } + if (keyboardAnimator.animating) { + // If Android is animating the keyboard in/out, forgo our own animation. + keyboardGuideline?.setGuidelineEnd(target) + return + } + 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 -> + (animation.animatedValue as? Int)?.let { currentValue -> + keyboardGuideline?.setGuidelineEnd(currentValue) + } + } + start() + } } private fun getKeyboardHeight(): Int { @@ -258,8 +287,14 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( return } + if (otherKeyboardAnimator?.isRunning == true) { + // Terminate other keyboard guideline animations as to not interfere with this one + otherKeyboardAnimator?.end() + otherKeyboardAnimator = null + } animating = true startingGuidelineEnd = keyboardGuideline.guidelineEnd + Log.d(TAG, "KeyboardInsetAnimator animating from $startingGuidelineEnd") } override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat { @@ -293,6 +328,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } keyboardGuideline?.setGuidelineEnd(endingGuidelineEnd) + Log.d(TAG, "KeyboardInsetAnimator animated to $endingGuidelineEnd") animating = false } } 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..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,32 +1,75 @@ package org.thoughtcrime.securesms.conversation -import androidx.core.view.doOnNextLayout -import androidx.recyclerview.widget.RecyclerView +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 /** - * Adds necessary padding to each side of the given RecyclerView in order to ensure that - * if all buttons can fit in the visible real-estate on screen, they are centered. + * 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 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. */ -class AttachmentButtonCenterHelper(private val recyclerView: RecyclerView) : RecyclerView.AdapterDataObserver() { - - 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 - val requiredSpace = itemWidth * itemCount - - recyclerView.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) - } - } else { - it.setPadding(defaultPadding.toInt(), it.paddingTop, defaultPadding.toInt(), it.paddingBottom) +class AttachmentButtonCenterHelper(val buttonHolder: View, val wrapper: View) { + + 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 + } + + fun recenter(buttonHolderCoreWidth: Int, wrapperWidth: Int) { + val extraSpace = wrapperWidth - buttonHolderCoreWidth + val horizontalPadding = if (extraSpace >= 0) + (extraSpace / 2f).toInt() + else + DEFAULT_PADDING + 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/AttachmentKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java index ad4bd0e3510..b14dbb7dcfd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java @@ -13,7 +13,6 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; @@ -44,9 +43,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; @@ -71,16 +70,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.setOnButtonClicked(button -> { if (callback != null) { callback.onAttachmentSelectorClicked(button); } @@ -97,14 +94,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) { @@ -113,9 +106,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())); } } @@ -165,7 +158,6 @@ public void setWallpaperEnabled(boolean wallpaperEnabled) { } else { container.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.signal_background_primary)); } - buttonAdapter.setWallpaperEnabled(wallpaperEnabled); } @Override @@ -264,4 +256,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/AttachmentKeyboardButton.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java index 63bc50ff3c7..17ea169a912 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java @@ -16,6 +16,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/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..05b6a027b3f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonList.kt @@ -0,0 +1,69 @@ +/* + * 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.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 java.util.function.Consumer + +class AttachmentKeyboardButtonList @JvmOverloads constructor( + context: Context, + 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 recenterHelper: AttachmentButtonCenterHelper + 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) + recenterHelper = AttachmentButtonCenterHelper(inner, this) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + recenterHelper.attach() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + recenterHelper.detach() + } + + fun setButtons(newButtons: List) { + if (currentButtons == newButtons) + return + Log.d(TAG, "setButtons: $currentButtons -> $newButtons") + currentButtons = newButtons + inner.removeAllViews() + newButtons.forEach { inner += inflateButton(it) } + } + + 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) + setOnClickListener { onButtonClicked.accept(button) } + } + } +} 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 fe40a0b384b..747f8396cd4 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 @@ -164,6 +164,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 @@ -392,6 +393,7 @@ class ConversationFragment : StickerEventListener, StickerKeyboardPageFragment.Callback, MediaKeyboard.MediaKeyboardListener, + AttachmentKeyboard.AttachmentKeyboardListener, EmojiSearchFragment.Callback, ScheduleMessageTimePickerBottomSheet.ScheduleCallback, ScheduleMessageDialogCallback, @@ -905,6 +907,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 b43522c6eb6..acf4bd702ce 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,6 +28,7 @@ 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 org.thoughtcrime.securesms.util.RemoteConfig import java.util.function.Predicate @@ -34,7 +36,7 @@ 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" @@ -143,4 +145,12 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_ attachmentKeyboardView.filterAttachmentKeyboardButtons(null) } } + + override fun show() { + findListener()?.onAttachmentKeyboardShown() + } + + override fun hide() { + findListener()?.onAttachmentKeyboardHidden() + } } 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..ff7fbd2e1d2 --- /dev/null +++ b/app/src/main/res/anim/fade_out_slowly.xml @@ -0,0 +1,7 @@ + + 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..b2864a93d18 --- /dev/null +++ b/app/src/main/res/layout/attachment_keyboard_button_list.xml @@ -0,0 +1,13 @@ + + + + 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 diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index bc3ee9a344e..f85ea312bf9 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -9,4 +9,7 @@ 5000 150 - \ No newline at end of file + + + 160 +