Retain TextView selection after list updating
This commit is contained in:
		
							parent
							
								
									3e6747c880
								
							
						
					
					
						commit
						858a327299
					
				|  | @ -12,8 +12,6 @@ import android.content.IntentSender.SendIntentException; | |||
| import android.os.Bundle; | ||||
| import android.os.Handler; | ||||
| import android.text.InputType; | ||||
| import android.text.Selection; | ||||
| import android.text.Spannable; | ||||
| import android.util.Log; | ||||
| import android.util.Pair; | ||||
| import android.view.ContextMenu; | ||||
|  | @ -40,8 +38,6 @@ import android.widget.Toast; | |||
| 
 | ||||
| import net.java.otr4j.session.SessionStatus; | ||||
| 
 | ||||
| import java.lang.reflect.Field; | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| 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.OnContactPictureClicked; | ||||
| 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.UIHelper; | ||||
| import eu.siacs.conversations.xmpp.XmppConnection; | ||||
|  | @ -553,7 +550,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa | |||
| 					&& !GeoHelper.isGeoUri(m.getBody()) | ||||
| 					&& m.treatAsDownloadable() != Message.Decision.MUST) { | ||||
| 				copyText.setVisible(true); | ||||
| 				selectText.setVisible(METHOD_START_SELECTION != null); | ||||
| 				selectText.setVisible(ListSelectionManager.isSupported()); | ||||
| 			} | ||||
| 			if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { | ||||
| 				retryDecryption.setVisible(true); | ||||
|  | @ -677,13 +674,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa | |||
| 				final View view = this.messagesView.getChildAt(index - first); | ||||
| 				final TextView messageBody = this.messageListAdapter.getMessageBody(view); | ||||
| 				if (messageBody != null) { | ||||
| 					final Spannable text = (Spannable) messageBody.getText(); | ||||
| 					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) { | ||||
| 					} | ||||
| 					ListSelectionManager.startSelection(messageBody); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | @ -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; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -53,6 +53,7 @@ import eu.siacs.conversations.entities.Message.FileParams; | |||
| import eu.siacs.conversations.entities.Transferable; | ||||
| import eu.siacs.conversations.ui.ConversationActivity; | ||||
| 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.GeoHelper; | ||||
| import eu.siacs.conversations.utils.UIHelper; | ||||
|  | @ -86,6 +87,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { | |||
| 	private boolean mIndicateReceived = false; | ||||
| 	private boolean mUseGreenBackground = false; | ||||
| 
 | ||||
| 	private final ListSelectionManager listSelectionManager = new ListSelectionManager(); | ||||
| 
 | ||||
| 	public MessageAdapter(ConversationActivity activity, List<Message> messages) { | ||||
| 		super(activity, 0, messages); | ||||
| 		this.activity = activity; | ||||
|  | @ -361,6 +364,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { | |||
| 			viewHolder.messageBody.setText(formattedBody); | ||||
| 			viewHolder.messageBody.setTextIsSelectable(true); | ||||
| 			viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); | ||||
| 			listSelectionManager.onUpdate(viewHolder.messageBody, message); | ||||
| 		} else { | ||||
| 			viewHolder.messageBody.setText(""); | ||||
| 			viewHolder.messageBody.setTextIsSelectable(false); | ||||
|  | @ -534,6 +538,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { | |||
| 					viewHolder = null; | ||||
| 					break; | ||||
| 			} | ||||
| 			if (viewHolder.messageBody != null) listSelectionManager.onCreate(viewHolder.messageBody); | ||||
| 			view.setTag(viewHolder); | ||||
| 		} else { | ||||
| 			viewHolder = (ViewHolder) view.getTag(); | ||||
|  | @ -684,6 +689,13 @@ public class MessageAdapter extends ArrayAdapter<Message> { | |||
| 		return view; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void notifyDataSetChanged() { | ||||
| 		listSelectionManager.onBeforeNotifyDataSetChanged(); | ||||
| 		super.notifyDataSetChanged(); | ||||
| 		listSelectionManager.onAfterNotifyDataSetChanged(); | ||||
| 	} | ||||
| 
 | ||||
| 	public void openDownloadable(Message message) { | ||||
| 		DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); | ||||
| 		if (!file.exists()) { | ||||
|  |  | |||
|  | @ -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) { | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	 Mishiranu
						Mishiranu