From abcdd96cc90149435f09e2efd214d54f5b1861de Mon Sep 17 00:00:00 2001 From: kosyak Date: Sun, 18 Feb 2024 23:32:44 +0100 Subject: [PATCH] proper swipe to reply handling --- .../com/cheogram/android/SwipeDetector.java | 95 --------------- .../conversations/ui/DraggableListView.kt | 62 ++++++++++ .../siacs/conversations/ui/LockedViewPager.kt | 23 ++++ .../ui/adapter/MessageAdapter.java | 109 +++++++++++++----- src/main/res/layout/fragment_conversation.xml | 10 +- src/main/res/values/ids.xml | 1 + 6 files changed, 175 insertions(+), 125 deletions(-) delete mode 100644 src/main/java/com/cheogram/android/SwipeDetector.java create mode 100644 src/main/java/eu/siacs/conversations/ui/DraggableListView.kt create mode 100644 src/main/java/eu/siacs/conversations/ui/LockedViewPager.kt diff --git a/src/main/java/com/cheogram/android/SwipeDetector.java b/src/main/java/com/cheogram/android/SwipeDetector.java deleted file mode 100644 index 93c2405c8..000000000 --- a/src/main/java/com/cheogram/android/SwipeDetector.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.cheogram.android; - -import android.content.res.Resources; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; - -import java.util.Set; - -import eu.siacs.conversations.utils.Consumer; - -// https://stackoverflow.com/a/41766670/8611 -/** - * Created by hoshyar on 1/19/17. - */ - -public class SwipeDetector implements View.OnTouchListener { - - protected Consumer cb; - - private Set allowedActions; - - private int touchSlop = -1; - - public SwipeDetector(Consumer cb, Set allowedActions) { - this.cb = cb; - this.allowedActions = allowedActions; - } - - public static enum Action { - LR, // Left to Right - RL, // Right to Left - None // when no action was detected - } - - private static final String logTag = "Swipe"; - private static final int MIN_DISTANCE = 100; - private float downX, downY, upX, upY; - private Action mSwipeDetected = Action.None; - - public boolean swipeDetected() { - return mSwipeDetected != Action.None; - } - - public Action getAction() { - return mSwipeDetected; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (touchSlop == -1) { - touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); - } - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - downX = event.getX(); - downY = event.getY(); - mSwipeDetected = Action.None; - return false; - - case MotionEvent.ACTION_MOVE: - upX = event.getX(); - upY = event.getY(); - - float deltaX = downX - upX; - float deltaY = downY - upY; - - if ( - (allowedActions.contains(Action.LR) && deltaX < -touchSlop || - allowedActions.contains(Action.RL) && deltaX > touchSlop) && Math.abs(deltaX) > Math.abs(deltaY) - ) { - v.getParent().requestDisallowInterceptTouchEvent(true); - } - - if (Math.abs(deltaX) > dpToPx(MIN_DISTANCE)) { - // left or right - if (deltaX < 0 && allowedActions.contains(Action.LR)) { - cb.accept(mSwipeDetected = Action.LR); - return true; - } - if (deltaX > 0 && allowedActions.contains(Action.RL)) { - cb.accept(mSwipeDetected = Action.RL); - return true; - } - } - return false; - } - return false; - } - - private static int dpToPx(int dp) { - return (int) (dp * Resources.getSystem().getDisplayMetrics().density); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/DraggableListView.kt b/src/main/java/eu/siacs/conversations/ui/DraggableListView.kt new file mode 100644 index 000000000..cfa6a3508 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/DraggableListView.kt @@ -0,0 +1,62 @@ +package eu.siacs.conversations.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.ListAdapter +import android.widget.ListView +import androidx.customview.widget.ViewDragHelper + +class DraggableListView : ListView { + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + private var dragHelper: ViewDragHelper? = null + + override fun setAdapter(adapter: ListAdapter?) { + super.setAdapter(adapter) + val dragHelperCallback = if (adapter is DraggableAdapter) { + adapter.getDragCallback() + } else { + null + } + + dragHelper = if (dragHelperCallback != null) { + ViewDragHelper.create(this, dragHelperCallback) + } else { + null + } + + if (adapter is DraggableAdapter) { + adapter.setViewDragHelper(dragHelper) + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (dragHelper?.shouldInterceptTouchEvent(ev) == true) { + return true + } + + return super.onInterceptTouchEvent(ev) + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + val res = dragHelper?.viewDragState != ViewDragHelper.STATE_DRAGGING && super.onTouchEvent(ev) + return if (res) { + true + } else { + dragHelper?.processTouchEvent(ev) + dragHelper?.viewDragState == ViewDragHelper.STATE_DRAGGING + } + } + + interface DraggableAdapter { + fun getDragCallback(): ViewDragHelper.Callback? + fun setViewDragHelper(helper: ViewDragHelper?) + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/LockedViewPager.kt b/src/main/java/eu/siacs/conversations/ui/LockedViewPager.kt new file mode 100644 index 000000000..82b768557 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/LockedViewPager.kt @@ -0,0 +1,23 @@ +package eu.siacs.conversations.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.viewpager.widget.ViewPager + +class LockedViewPager : ViewPager { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + override fun onTouchEvent(event: MotionEvent): Boolean { + return false + } + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + return false + } + + override fun canScrollHorizontally(direction: Int): Boolean { + return false + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 2b86f9e8a..774983c5f 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -6,7 +6,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; -import android.graphics.Outline; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; @@ -16,7 +15,6 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextPaint; import android.text.format.DateUtils; -import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; @@ -25,7 +23,6 @@ import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; -import android.view.ViewOutlineProvider; import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.Button; @@ -41,17 +38,15 @@ import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.customview.widget.ViewDragHelper; -import com.cheogram.android.SwipeDetector; import com.google.common.base.Strings; -import org.checkerframework.checker.units.qual.C; - import java.net.URI; import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -71,6 +66,7 @@ import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.ConversationsActivity; +import eu.siacs.conversations.ui.DraggableListView; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.service.AudioPlayer; import eu.siacs.conversations.ui.text.DividerSpan; @@ -92,7 +88,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.mam.MamReference; -public class MessageAdapter extends ArrayAdapter { +public class MessageAdapter extends ArrayAdapter implements DraggableListView.DraggableAdapter { public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; private static final int SENT = 0; @@ -113,13 +109,67 @@ public class MessageAdapter extends ArrayAdapter { private boolean mUseGreenBackground = false; private final boolean mForceNames; - private Set allowedSwipeActions; - @ColorInt private int primaryColor = -1; private boolean allowRelativeTimestamps = true; + private ViewDragHelper dragHelper = null; + private final ViewDragHelper.Callback dragCallback = new ViewDragHelper.Callback() { + @Override + public boolean tryCaptureView(@NonNull View child, int pointerId) { + return child.getTag(R.id.TAG_DRAGGABLE) != null; + } + + @Override + public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) { + if (dragHelper != null) { + dragHelper.settleCapturedViewAt(0, releasedChild.getTop()); + ViewCompat.postOnAnimation(releasedChild, new SettleRunnable(releasedChild)); + ViewHolder viewHolder = (ViewHolder) releasedChild.getTag(); + + if (viewHolder != null && viewHolder.position >= 0 && viewHolder.position < getCount()) { + Message m = getItem(viewHolder.position); + if (messageBoxSwipedListener != null) { + messageBoxSwipedListener.onMessageBoxSwiped(m); + } + } + } + super.onViewReleased(releasedChild, xvel, yvel); + } + + @Override + public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) { + return Math.max(-child.getWidth()/4, Math.min(left, 0)); + } + + + @Override + public int clampViewPositionVertical(@NonNull View child, int top, int dy) { + return child.getTop(); + } + + @Override + public int getViewHorizontalDragRange(@NonNull View child) { + return Math.max(Math.abs(child.getLeft()), 1); + } + + private class SettleRunnable implements Runnable { + private View view; + + public SettleRunnable(View view) { + this.view = view; + } + + @Override + public void run() { + if (dragHelper != null && dragHelper.continueSettling(true)) { + ViewCompat.postOnAnimation(view, this); + } + } + } + }; + public MessageAdapter(final XmppActivity activity, final List messages, final boolean forceNames) { super(activity, 0, messages); this.audioPlayer = new AudioPlayer(this); @@ -127,8 +177,6 @@ public class MessageAdapter extends ArrayAdapter { metrics = getContext().getResources().getDisplayMetrics(); updatePreferences(); this.mForceNames = forceNames; - allowedSwipeActions = new HashSet<>(); - allowedSwipeActions.add(SwipeDetector.Action.RL); final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); allowRelativeTimestamps = !p.getBoolean("always_full_timestamps", activity.getResources().getBoolean(R.bool.always_full_timestamps)); @@ -138,6 +186,17 @@ public class MessageAdapter extends ArrayAdapter { this(activity, messages, false); } + @Nullable + @Override + public ViewDragHelper.Callback getDragCallback() { + return dragCallback; + } + + @Override + public void setViewDragHelper(@Nullable ViewDragHelper helper) { + this.dragHelper = helper; + } + @Override public boolean areAllItemsEnabled() { return false; @@ -761,10 +820,11 @@ public class MessageAdapter extends ArrayAdapter { final Conversational conversation = message.getConversation(); final Account account = conversation.getAccount(); final int type = getItemViewType(position); + ViewHolder viewHolder; if (view == null) { viewHolder = new ViewHolder(); - + viewHolder.position = position; switch (type) { case DATE_SEPARATOR: view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); @@ -794,6 +854,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.time = view.findViewById(R.id.message_time); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + view.setTag(R.id.TAG_DRAGGABLE, true); break; case RECEIVED: view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); @@ -810,6 +871,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); viewHolder.encryption = view.findViewById(R.id.message_encryption); viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + view.setTag(R.id.TAG_DRAGGABLE, true); break; case STATUS: view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); @@ -824,8 +886,14 @@ public class MessageAdapter extends ArrayAdapter { view.setTag(viewHolder); } else { viewHolder = (ViewHolder) view.getTag(); + if (dragHelper.getCapturedView() == view) { + dragHelper.abort(); + } + if (viewHolder == null) { return view; + } else { + viewHolder.position = position; } } @@ -924,19 +992,6 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.clicksInterceptor.setOnClickListener(messageItemClickListener); viewHolder.clicksInterceptor.setOnLongClickListener(messageItemLongClickListener); - - SwipeDetector swipeDetector = new SwipeDetector((action) -> { - if (action == SwipeDetector.Action.RL && MessageAdapter.this.messageBoxSwipedListener != null) { - MessageAdapter.this.messageBoxSwipedListener.onMessageBoxSwiped(message); - } - }, allowedSwipeActions); - - viewHolder.root.setOnTouchListener(swipeDetector); - viewHolder.message_box.setOnTouchListener(swipeDetector); - viewHolder.messageBody.setOnTouchListener(swipeDetector); - viewHolder.image.setOnTouchListener(swipeDetector); - viewHolder.time.setOnTouchListener(swipeDetector); - viewHolder.contact_picture.setOnClickListener(v -> { if (MessageAdapter.this.mOnContactPictureClickedListener != null) { MessageAdapter.this.mOnContactPictureClickedListener @@ -1215,5 +1270,7 @@ public class MessageAdapter extends ArrayAdapter { protected TextView encryption; protected View clicksInterceptor; + + int position; } } diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 63f5ab244..561c82602 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -21,7 +21,7 @@ app:tabSelectedTextColor="@color/white" app:tabTextColor="@color/white70" /> - - + android:layout_height="wrap_content" + android:maxLines="5" + android:ellipsize="end" /> @@ -269,7 +271,7 @@ - + \ No newline at end of file diff --git a/src/main/res/values/ids.xml b/src/main/res/values/ids.xml index 975b454ce..4ed99764b 100644 --- a/src/main/res/values/ids.xml +++ b/src/main/res/values/ids.xml @@ -4,4 +4,5 @@ + \ No newline at end of file