proper swipe to reply handling

This commit is contained in:
kosyak 2024-02-18 23:32:44 +01:00
parent 6c8d9c30ab
commit abcdd96cc9
6 changed files with 175 additions and 125 deletions

View file

@ -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<Action> cb;
private Set<Action> allowedActions;
private int touchSlop = -1;
public SwipeDetector(Consumer<Action> cb, Set<Action> 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);
}
}

View file

@ -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?)
}
}

View file

@ -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
}
}

View file

@ -6,7 +6,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
@ -16,7 +15,6 @@ import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan; import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
@ -25,7 +23,6 @@ import android.util.DisplayMetrics;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
@ -41,17 +38,15 @@ import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; 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 com.google.common.base.Strings;
import org.checkerframework.checker.units.qual.C;
import java.net.URI; import java.net.URI;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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.services.NotificationService;
import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.ConversationFragment;
import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.DraggableListView;
import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.service.AudioPlayer; import eu.siacs.conversations.ui.service.AudioPlayer;
import eu.siacs.conversations.ui.text.DividerSpan; 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.Jid;
import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.mam.MamReference;
public class MessageAdapter extends ArrayAdapter<Message> { public class MessageAdapter extends ArrayAdapter<Message> implements DraggableListView.DraggableAdapter {
public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
private static final int SENT = 0; private static final int SENT = 0;
@ -113,13 +109,67 @@ public class MessageAdapter extends ArrayAdapter<Message> {
private boolean mUseGreenBackground = false; private boolean mUseGreenBackground = false;
private final boolean mForceNames; private final boolean mForceNames;
private Set<SwipeDetector.Action> allowedSwipeActions;
@ColorInt @ColorInt
private int primaryColor = -1; private int primaryColor = -1;
private boolean allowRelativeTimestamps = true; 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<Message> messages, final boolean forceNames) { public MessageAdapter(final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
super(activity, 0, messages); super(activity, 0, messages);
this.audioPlayer = new AudioPlayer(this); this.audioPlayer = new AudioPlayer(this);
@ -127,8 +177,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
metrics = getContext().getResources().getDisplayMetrics(); metrics = getContext().getResources().getDisplayMetrics();
updatePreferences(); updatePreferences();
this.mForceNames = forceNames; this.mForceNames = forceNames;
allowedSwipeActions = new HashSet<>();
allowedSwipeActions.add(SwipeDetector.Action.RL);
final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity);
allowRelativeTimestamps = !p.getBoolean("always_full_timestamps", activity.getResources().getBoolean(R.bool.always_full_timestamps)); allowRelativeTimestamps = !p.getBoolean("always_full_timestamps", activity.getResources().getBoolean(R.bool.always_full_timestamps));
@ -138,6 +186,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
this(activity, messages, false); this(activity, messages, false);
} }
@Nullable
@Override
public ViewDragHelper.Callback getDragCallback() {
return dragCallback;
}
@Override
public void setViewDragHelper(@Nullable ViewDragHelper helper) {
this.dragHelper = helper;
}
@Override @Override
public boolean areAllItemsEnabled() { public boolean areAllItemsEnabled() {
return false; return false;
@ -761,10 +820,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
final Conversational conversation = message.getConversation(); final Conversational conversation = message.getConversation();
final Account account = conversation.getAccount(); final Account account = conversation.getAccount();
final int type = getItemViewType(position); final int type = getItemViewType(position);
ViewHolder viewHolder; ViewHolder viewHolder;
if (view == null) { if (view == null) {
viewHolder = new ViewHolder(); viewHolder = new ViewHolder();
viewHolder.position = position;
switch (type) { switch (type) {
case DATE_SEPARATOR: case DATE_SEPARATOR:
view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false);
@ -794,6 +854,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.time = view.findViewById(R.id.message_time); viewHolder.time = view.findViewById(R.id.message_time);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.audioPlayer = view.findViewById(R.id.audio_player); viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
view.setTag(R.id.TAG_DRAGGABLE, true);
break; break;
case RECEIVED: case RECEIVED:
view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false);
@ -810,6 +871,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.encryption = view.findViewById(R.id.message_encryption); viewHolder.encryption = view.findViewById(R.id.message_encryption);
viewHolder.audioPlayer = view.findViewById(R.id.audio_player); viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
view.setTag(R.id.TAG_DRAGGABLE, true);
break; break;
case STATUS: case STATUS:
view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
@ -824,8 +886,14 @@ public class MessageAdapter extends ArrayAdapter<Message> {
view.setTag(viewHolder); view.setTag(viewHolder);
} else { } else {
viewHolder = (ViewHolder) view.getTag(); viewHolder = (ViewHolder) view.getTag();
if (dragHelper.getCapturedView() == view) {
dragHelper.abort();
}
if (viewHolder == null) { if (viewHolder == null) {
return view; return view;
} else {
viewHolder.position = position;
} }
} }
@ -924,19 +992,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.clicksInterceptor.setOnClickListener(messageItemClickListener); viewHolder.clicksInterceptor.setOnClickListener(messageItemClickListener);
viewHolder.clicksInterceptor.setOnLongClickListener(messageItemLongClickListener); 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 -> { viewHolder.contact_picture.setOnClickListener(v -> {
if (MessageAdapter.this.mOnContactPictureClickedListener != null) { if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
MessageAdapter.this.mOnContactPictureClickedListener MessageAdapter.this.mOnContactPictureClickedListener
@ -1215,5 +1270,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected TextView encryption; protected TextView encryption;
protected View clicksInterceptor; protected View clicksInterceptor;
int position;
} }
} }

View file

@ -21,7 +21,7 @@
app:tabSelectedTextColor="@color/white" app:tabSelectedTextColor="@color/white"
app:tabTextColor="@color/white70" /> app:tabTextColor="@color/white70" />
<androidx.viewpager.widget.ViewPager <eu.siacs.conversations.ui.LockedViewPager
android:id="@+id/conversation_view_pager" android:id="@+id/conversation_view_pager"
android:layout_below="@id/tab_layout" android:layout_below="@id/tab_layout"
android:layout_width="fill_parent" android:layout_width="fill_parent"
@ -31,7 +31,7 @@
<RelativeLayout <RelativeLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent"> android:layout_height="fill_parent">
<ListView <eu.siacs.conversations.ui.DraggableListView
android:id="@+id/messages_view" android:id="@+id/messages_view"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -86,7 +86,9 @@
<TextView <TextView
android:id="@+id/context_preview_text" android:id="@+id/context_preview_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
android:maxLines="5"
android:ellipsize="end" />
</LinearLayout> </LinearLayout>
@ -269,7 +271,7 @@
</RelativeLayout> </RelativeLayout>
</androidx.viewpager.widget.ViewPager> </eu.siacs.conversations.ui.LockedViewPager>
</RelativeLayout> </RelativeLayout>
</layout> </layout>

View file

@ -4,4 +4,5 @@
<item type="id" name="TAG_FINGERPRINT"/> <item type="id" name="TAG_FINGERPRINT"/>
<item type="id" name="TAG_FINGERPRINT_STATUS"/> <item type="id" name="TAG_FINGERPRINT_STATUS"/>
<item type="id" name="TAG_AUDIO_PLAYER_VIEW_HOLDER"/> <item type="id" name="TAG_AUDIO_PLAYER_VIEW_HOLDER"/>
<item type="id" name="TAG_DRAGGABLE"/>
</resources> </resources>