Retain TextView selection after list updating

This commit is contained in:
Mishiranu 2016-09-18 16:35:14 +03:00
parent 3e6747c880
commit 858a327299
3 changed files with 216 additions and 39 deletions

View file

@ -12,8 +12,6 @@ import android.content.IntentSender.SendIntentException;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.text.InputType; import android.text.InputType;
import android.text.Selection;
import android.text.Spannable;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.ContextMenu; import android.view.ContextMenu;
@ -40,8 +38,6 @@ import android.widget.Toast;
import net.java.otr4j.session.SessionStatus; import net.java.otr4j.session.SessionStatus;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -66,6 +62,7 @@ import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter;
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
@ -553,7 +550,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
&& !GeoHelper.isGeoUri(m.getBody()) && !GeoHelper.isGeoUri(m.getBody())
&& m.treatAsDownloadable() != Message.Decision.MUST) { && m.treatAsDownloadable() != Message.Decision.MUST) {
copyText.setVisible(true); copyText.setVisible(true);
selectText.setVisible(METHOD_START_SELECTION != null); selectText.setVisible(ListSelectionManager.isSupported());
} }
if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
retryDecryption.setVisible(true); retryDecryption.setVisible(true);
@ -677,13 +674,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
final View view = this.messagesView.getChildAt(index - first); final View view = this.messagesView.getChildAt(index - first);
final TextView messageBody = this.messageListAdapter.getMessageBody(view); final TextView messageBody = this.messageListAdapter.getMessageBody(view);
if (messageBody != null) { if (messageBody != null) {
final Spannable text = (Spannable) messageBody.getText(); ListSelectionManager.startSelection(messageBody);
Selection.setSelection(text, 0, text.length());
try {
Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(messageBody) : messageBody;
METHOD_START_SELECTION.invoke(editor);
} catch (Exception e) {
}
} }
} }
} }
@ -1463,31 +1454,4 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} }
} }
private static final Field FIELD_EDITOR;
private static final Method METHOD_START_SELECTION;
static {
Field editor;
try {
editor = TextView.class.getDeclaredField("mEditor");
editor.setAccessible(true);
} catch (Exception e) {
editor = null;
}
FIELD_EDITOR = editor;
Class<?> editorClass = editor != null ? editor.getType() : TextView.class;
String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
Method startSelection = null;
for (String startSelectionName : startSelectionNames) {
try {
startSelection = editorClass.getDeclaredMethod(startSelectionName);
startSelection.setAccessible(true);
break;
} catch (Exception e) {
startSelection = null;
}
}
METHOD_START_SELECTION = startSelection;
}
} }

View file

@ -53,6 +53,7 @@ import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
@ -86,6 +87,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
private boolean mIndicateReceived = false; private boolean mIndicateReceived = false;
private boolean mUseGreenBackground = false; private boolean mUseGreenBackground = false;
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
public MessageAdapter(ConversationActivity activity, List<Message> messages) { public MessageAdapter(ConversationActivity activity, List<Message> messages) {
super(activity, 0, messages); super(activity, 0, messages);
this.activity = activity; this.activity = activity;
@ -361,6 +364,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setText(formattedBody); viewHolder.messageBody.setText(formattedBody);
viewHolder.messageBody.setTextIsSelectable(true); viewHolder.messageBody.setTextIsSelectable(true);
viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
listSelectionManager.onUpdate(viewHolder.messageBody, message);
} else { } else {
viewHolder.messageBody.setText(""); viewHolder.messageBody.setText("");
viewHolder.messageBody.setTextIsSelectable(false); viewHolder.messageBody.setTextIsSelectable(false);
@ -534,6 +538,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder = null; viewHolder = null;
break; break;
} }
if (viewHolder.messageBody != null) listSelectionManager.onCreate(viewHolder.messageBody);
view.setTag(viewHolder); view.setTag(viewHolder);
} else { } else {
viewHolder = (ViewHolder) view.getTag(); viewHolder = (ViewHolder) view.getTag();
@ -684,6 +689,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
return view; return view;
} }
@Override
public void notifyDataSetChanged() {
listSelectionManager.onBeforeNotifyDataSetChanged();
super.notifyDataSetChanged();
listSelectionManager.onAfterNotifyDataSetChanged();
}
public void openDownloadable(Message message) { public void openDownloadable(Message message) {
DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
if (!file.exists()) { if (!file.exists()) {

View file

@ -0,0 +1,201 @@
package eu.siacs.conversations.ui.widget;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.Selection;
import android.text.Spannable;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
public class ListSelectionManager {
private static final int MESSAGE_SEND_RESET = 1;
private static final int MESSAGE_RESET = 2;
private static final int MESSAGE_START_SELECTION = 3;
private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SEND_RESET: {
// Skip one more message queue loop
HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget();
return true;
}
case MESSAGE_RESET: {
final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj;
listSelectionManager.futureSelectionIdentifier = null;
return true;
}
case MESSAGE_START_SELECTION: {
final StartSelectionHolder holder = (StartSelectionHolder) msg.obj;
holder.listSelectionManager.futureSelectionIdentifier = null;
startSelection(holder.textView, holder.start, holder.end);
return true;
}
}
return false;
}
});
private static class StartSelectionHolder {
public final ListSelectionManager listSelectionManager;
public final TextView textView;
public final int start;
public final int end;
public StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
int start, int end) {
this.listSelectionManager = listSelectionManager;
this.textView = textView;
this.start = start;
this.end = end;
}
}
private ActionMode selectionActionMode;
private Object selectionIdentifier;
private TextView selectionTextView;
private Object futureSelectionIdentifier;
private int futureSelectionStart;
private int futureSelectionEnd;
public void onCreate(TextView textView) {
final CustomCallback callback = new CustomCallback(textView);
textView.setCustomSelectionActionModeCallback(callback);
}
public void onUpdate(TextView textView, Object identifier) {
if (SUPPORTED) {
CustomCallback callback = (CustomCallback) textView.getCustomSelectionActionModeCallback();
callback.identifier = identifier;
if (futureSelectionIdentifier == identifier) {
HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
}
}
}
public void onBeforeNotifyDataSetChanged() {
if (SUPPORTED) {
HANDLER.removeMessages(MESSAGE_SEND_RESET);
HANDLER.removeMessages(MESSAGE_RESET);
HANDLER.removeMessages(MESSAGE_START_SELECTION);
if (selectionActionMode != null) {
final CharSequence text = selectionTextView.getText();
futureSelectionIdentifier = selectionIdentifier;
futureSelectionStart = Selection.getSelectionStart(text);
futureSelectionEnd = Selection.getSelectionEnd(text);
selectionActionMode.finish();
selectionActionMode = null;
selectionIdentifier = null;
selectionTextView = null;
}
}
}
public void onAfterNotifyDataSetChanged() {
if (SUPPORTED && futureSelectionIdentifier != null) {
HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
}
}
private class CustomCallback implements ActionMode.Callback {
private final TextView textView;
public Object identifier;
public CustomCallback(TextView textView) {
this.textView = textView;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
selectionActionMode = mode;
selectionIdentifier = identifier;
selectionTextView = textView;
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
if (selectionActionMode == mode) {
selectionActionMode = null;
selectionIdentifier = null;
selectionTextView = null;
}
}
}
private static final Field FIELD_EDITOR;
private static final Method METHOD_START_SELECTION;
private static final boolean SUPPORTED;
static {
Field editor;
try {
editor = TextView.class.getDeclaredField("mEditor");
editor.setAccessible(true);
} catch (Exception e) {
editor = null;
}
FIELD_EDITOR = editor;
Method startSelection = null;
if (editor != null) {
String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
for (String startSelectionName : startSelectionNames) {
try {
startSelection = editor.getType().getDeclaredMethod(startSelectionName);
startSelection.setAccessible(true);
break;
} catch (Exception e) {
startSelection = null;
}
}
}
METHOD_START_SELECTION = startSelection;
SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
}
public static boolean isSupported() {
return SUPPORTED;
}
public static void startSelection(TextView textView) {
startSelection(textView, 0, textView.getText().length());
}
public static void startSelection(TextView textView, int start, int end) {
final CharSequence text = textView.getText();
if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
final Spannable spannable = (Spannable) text;
start = Math.min(start, spannable.length());
end = Math.min(end, spannable.length());
Selection.setSelection(spannable, start, end);
try {
final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView;
METHOD_START_SELECTION.invoke(editor);
} catch (Exception e) {
}
}
}
}