- Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
- This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
1. Introduction
National Day fishing, is also not idle, he remembered before doing a class notes app, text to the system to choose options menu to add a new entry, click the entrance, you can select text content, pass to our app (some friend already know it can do many things), thought, to share with you, Specific use is actually very simple, and listen to us slowly, do not try to row away, there is a realization of the principle of analysis
Below we from how to use and source these two angles to expand the introduction: ActionMode and system text selection menu, as for why put these two together, I believe you after reading this article, will have their own opinions
2.ActionMode
Click on the “Official ActionMode User Guide”
Why should I write an official manual when there is one? 2. Write examples and play with 🤣😅
ActionMode is an abstract class that focuses user interaction on performing association operations. ActionMode has two modes: TYPE_PRIMARY(default mode) and TYPE_FLOATING(floating toolbar).
A. How to use it
ActionMode has a Callback inside, which the comment says is available
View.startActionMode(actionmode. Callback) or view. startActionMode(actionmode. Callback,int type)
The startActionMode in the Activity is also the view. startActionMode in the call
It is also very simple to use, as shown in the following example:
- 1. Resource XML that provides a context menu
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:title="@string/menu_favorite"
android:id="@+id/item_action_favorite"
android:icon="@drawable/ic_favorite_svg"/>
<item
android:title="@string/menu_cjsh"
android:id="@+id/item_action_cjsh"/>
</menu>
Copy the code
- 2. Implement
ActionMode.Callback
interface
var actionMode:ActionMode? = null
val actionModeCallback = object: ActionMode.Callback{
override fun onCreateActionMode(mode: ActionMode? , menu:Menu?).: Boolean {
// Provides a menu resource for context menu items, as described in the official documentation user guidemode? .menuInflater? .inflate(R.menu.context_menu,menu)return true
}
override fun onPrepareActionMode(mode: ActionMode? , menu:Menu?).: Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode? , item:MenuItem?).: Boolean {
return when(item? .itemId) { R.id.item_action_favorite -> { Toast.makeText(applicationContext,"❤️ collected successfully",Toast.LENGTH_SHORT).show() mode? .finish()true}...else -> false}}override fun onDestroyActionMode(mode: ActionMode?). {
actionMode = null}}Copy the code
- 3. Enable associated operation mode
//type = TYPE_PRIMARY
actionMode = it.startActionMode(actionModeCallback)
//type = TYPE_FLOATING
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
actionMode = it.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING)
}
Copy the code
Example – Demonstrates the effect
B. Source code analysis
When view. startActionMode is called, it goes down here
//android.view.View
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
ViewParent parent = getParent();
if (parent == null) return null;
try {
// Start recursively calling ViewGroup's startActionModeForChild
return parent.startActionModeForChild(this, callback, type);
} catch (AbstractMethodError ame) {
// Use the default actionmode. TYPE_PRIMARY to start the operation mode for the specified view
return parent.startActionModeForChild(this, callback); }}Copy the code
A recursive call to ViewGroup’s startActionModeForChild is eventually executed in the DecorView’s startActionMode method
//com.android.internal.policy.DecorView
private ActionMode startActionMode(
View originatingView, ActionMode.Callback callback, int type) {
ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
ActionMode mode = null;
/ / mWindow PhoneWindow
if(mWindow.getCallback() ! =null && !mWindow.isDestroyed()) {
try {
/ / here will trigger AppCompatWindowCallback# onWindowStartingActionMode
mode = mWindow.getCallback().onWindowStartingActionMode(wrappedCallback, type);
} catch(AbstractMethodError ame) { ...... }}if(mode ! =null) {... }else {
// In the example of this article
// This is where type= actionmode. TYPE_FLOATING is executed when called startMode
// Internally execute createFloatingActionMode
// Then internally initialize a FloatingToolbar and return FloatingActionMode
//FloatingToolbar: is a FloatingToolbar that displays context menu items, internally implemented using popWindow
mode = createActionMode(type, wrappedCallback, originatingView);
if(mode ! =null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {
setHandledActionMode(mode);
} else {
mode = null; }}if(mode ! =null&& mWindow.getCallback() ! =null && !mWindow.isDestroyed()) {
try {
// Call back the empty implementation of the onActionModeStarted method in the Activity
// Notifies the Activity that ActionMode has started
mWindow.getCallback().onActionModeStarted(mode);
} catch (AbstractMethodError ame) {
}
}
return mode;
}
Copy the code
We have a look at AppCompatWindowCallback# onWindowStartingActionMode internal implementation
//androidx.appcompat.app.AppCompatDelegateImpl.AppCompatWindowCallback
public android.view.ActionMode onWindowStartingActionMode(
android.view.ActionMode.Callback callback, int type) {
if (isHandleNativeActionModesEnabled()) {
switch (type) {
case android.view.ActionMode.TYPE_PRIMARY:
The TYPE_PRIMARY type triggers this method call
returnstartAsSupportActionMode(callback); }}/ / does not meet the above conditions, onWindowStartingActionMode will perform to the Activity
return super.onWindowStartingActionMode(callback, type);
}
Copy the code
Take a look at startAsSupportActionMode
//androidx.appcompat.app.AppCompatDelegateImpl.AppCompatWindowCallback
final android.view.ActionMode startAsSupportActionMode( android.view.ActionMode.Callback callback) {
Actionmode.callback wrapper
final SupportActionModeWrapper.CallbackWrapper callbackWrapper =
new SupportActionModeWrapper.CallbackWrapper(mContext, callback);
// Scroll down, there is analysis
final androidx.appcompat.view.ActionMode supportActionMode =
startSupportActionMode(callbackWrapper);
if(supportActionMode ! =null) {
// Return to the wrapped ActionMode
return callbackWrapper.getActionModeWrapper(supportActionMode);
}
return null;
}
Copy the code
Let’s take a look at the startSupportActionMode above
//androidx.appcompat.app.AppCompatDelegateImpl
public ActionMode startSupportActionMode(@NonNull final ActionMode.Callback callback) {...// Wrap Callback to clear internal references when action Mode is destroyed
final ActionMode.Callback wrappedCallback = new ActionModeCallbackWrapperV9(callback);
ActionBar ab = getSupportActionBar();
if(ab ! =null) {
// The supportActionBar here is WindowDecorActionBarmActionMode = ab.startActionMode(wrappedCallback); . }...return mActionMode;
}
Copy the code
Ab. StartActionMode (wrappedCallback) is implemented as follows
//androidx.appcompat.app.WindowDecorActionBar
public ActionMode startActionMode(ActionMode.Callback callback) {...// MenuBuilder is initialized internally and menuBuilder.callback is bound
ActionModeImpl mode = new ActionModeImpl(mContextView.getContext(), callback);
WindowDecorActionBar#dispatchOnCreate
// this will eventually Callback to actionmode.callback in our example demo above
// implement the onCreateActionMode method, then parse menu. XML and populate it with XML in menu
if (mode.dispatchOnCreate()) {
mActionMode = mode;
// State changes or view updates
mode.invalidate();
/ / mContextView ActionBarContextView
// Initialize a View that returns button mClose and add mClose to ActionBarContextView
// Get mMenuView from mMenuLayoutRes
// Then initialize multiple MenuViews. itemViews and fill them with mmenuViews and update the position of the MenuView in the container
// Execute mmenuView. requestLayout to refresh the view
mContextView.initForMode(mode);
// Perform VISIBLE animation for DecorToolBar and AppBarContextView
animateToMode(true);
// Send events for window state changes (only aware when using AccessbilityService)
mContextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
return mode;
}
return null;
}
Copy the code
C. summary
(1) After.view. startActionMode is called, Through will eventually perform recursive calls to DecorView startActionMode method (2) DecorView internal calls AppCompatWindowCallback# onWindowStartingActionMode when type = The ActionMode. TYPE_PRIMARY: Internally fire WindowDecorActionBar#startActionMode, then initialize ActionModeImpl and execute the dispatchOnCreate method, The dispatchOnCreate method will eventually Callback to the onCreateActionMode method inside the ActionMode.Callback interface, The developer calls menuconstructor.inflate to populate the XML into the Menu (MenuBuilder) and then adds mClose(ImageView) to the ActionBarContextView to initialize the mMenuView, ItemView and update the order of the view position. Call requestLayout to refresh the view. Implement VISIBLE animation of DecorToolBar’s INVISIBLE and AppBarContextView to control hiding and display; When type = actionmode. TYPE_FLOATING: MWindow. GetCallback (.) onWindowStartingActionMode method (namely AppCompatWindowCallback# onWindowStartingActionMode) returns null, The createActionMode method is then executed and internally the createFloatingActionMode method is executed: initialize a FloatingToolbar and return FloatingActionMode; FloatingToolbar: is a FloatingToolbar that displays context menu items, internally implemented via popWindow;
3. System text selection menu
Let’s take a look at the following two questions:
- A. How to pop up the system text selection menu? System text selection menu inside is how to achieve?
- B. How to add your own app’s options to the system text selection menu?
A. How do I display the system text selection menu? And internal implementation
For example, when we use TextView to display text, if we want the content to be selected, we can display the copy and select button. In this case, we can use the TextView method setTextIsSelectable(Boolean Selectable). How does it work? Let’s open up the TextView source and look at the setTextIsSelectable method
//android.widget.TextView
public void setTextIsSelectable(boolean selectable) {
if(! selectable && mEditor ==null) return;
// Initialize the Editor
createEditorIfNeeded();
// Prevent duplicate Settings
if (mEditor.mTextIsSelectable == selectable) return;
/ / update the mTextIsSelectablemEditor.mTextIsSelectable = selectable; . }Copy the code
The mTextIsSelectable () variable was used when the Editor’s inner class TextActionModeCallback was initialized. ActionMode was also used in this class
//android.widget.Editor
void startInsertionActionMode(a) {... ActionMode.Callback actionModeCallback =new TextActionModeCallback(TextActionMode.INSERTION);
// The ActionMode of TYPE_FLOATING is launched, and the popWindow is implemented inside
mTextActionMode = mTextView.startActionMode(
actionModeCallback, ActionMode.TYPE_FLOATING);
if(mTextActionMode ! =null&& getInsertionController() ! =null) {
// If the cursor insertion controller exists, a new menu will pop up for the user to paste contentgetInsertionController().show(); }}Copy the code
B. How to add your own app’s options to the system text selection menu?
We need to figure out where does the system add these options? So TextView, again, we mentioned TextActionModeCallback, and we know from ActionMode analysis that in onCreateActionMode, the contents of menu. XML will be filled into menu, So how does the system come with it? Read on for analysis:
//android.widget.Editor.TextActionModeCallback
private class TextActionModeCallback extends ActionMode.Callback2 {...@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {...// The menu options are dynamically added by the system, such as: cut, paste, copy, share, etc
populateMenuWithItems(menu);
Callback customCallback = getCustomCallback();
if(customCallback ! =null) {
if(! customCallback.onCreateActionMode(mode, menu)) {// If the above condition is true, deselect the text
Selection.setSelection((Spannable) mTextView.getText(),
mTextView.getSelectionEnd());
return false; }}if (mTextView.canProcessText()) {
// If the current text supports sharing, copying, and the length is greater than 0 && the selected length is greater than 0
ACTION_PROCESS_TEXT (intent.action_process_text, intent.action_process_text, intent.action_process_textmProcessTextIntentActionsHandler.onInitializeMenu(menu); }...return true; }...private void populateMenuWithItems(Menu menu) {
if (mTextView.canCut()) {/ / shearmenu.add(Menu.NONE, TextView.ID_CUT,......) ; }if (mTextView.canCopy()) {/ / copymenu.add(Menu.NONE, TextView.ID_COPY, ......) ; }...if (mTextView.canRequestAutofill()) {// Auto-fillmenu.add(Menu.NONE, TextView.ID_AUTOFILL,......) ; }if (mTextView.canPasteAsPlainText()) {/ / pastemenu.add(Menu.NONE,TextView.ID_PASTE_AS_PLAIN_TEXT,......) ; }... }... }Copy the code
We see mProcessTextIntentActionsHandler. OnInitializeMenu (menu) this method
//android.widget.Editor.ProcessTextIntentActionsHandler
public void onInitializeMenu(Menu menu) {
// Load the list of all action activities containing PROCESS_TEXT
loadSupportedActivities();
final int size = mSupportedActivities.size();
for (int i = 0; i < size; i++) {
final ResolveInfo resolveInfo = mSupportedActivities.get(i);
// Get the supported list and add it dynamically to menumenu.add(Menu.NONE, Menu.NONE, Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i, getLabel(resolveInfo)) .setIntent(createProcessTextIntentForResolveInfo(resolveInfo)) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); }}// Load all intents that support processing PROCESS_TEXT
private void loadSupportedActivities(a) {
mSupportedActivities.clear();
if(! mContext.canStartActivityForResult()) {return;
}
PackageManager packageManager = mTextView.getContext().getPackageManager();
// Query the intent of the condition
List<ResolveInfo> unfiltered =
packageManager.queryIntentActivities(createProcessTextIntent(), 0);
for (ResolveInfo info : unfiltered) {
if(isSupportedActivity(info)) { mSupportedActivities.add(info); }}}// Handle the Intent for PROCESS_TEXT
private Intent createProcessTextIntent(a) {
return new Intent()
.setAction(Intent.ACTION_PROCESS_TEXT)
.setType("text/plain");
}
Copy the code
Combined with the ActionMode content we analyzed above, this is all understood, so we add an option entry to the text selection menu of the system is not very simple?
- 1. Add the intent filter to androidmanifest.xml
<activity
android:name=".CustomTextActivity"
android:excludeFromRecents="false"
android:configChanges="locale|orientation|keyboardHidden|screenSize"
android:label="Like ❤️+ collect ❤️= institute ❤️">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
Copy the code
- 2. Deal with intent
class CustomTextActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_text)
val selectedText = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) intent? .getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)else null
textShowContent.text = "$selectedText\n\n Text content length:${selectedText? .length? :0}"}}Copy the code
System Text Selection menu – Add options (demo example)
Previous articles recommended: 3.Jetpack Compose UI creation layout drawing process + principle — detailed explanation of the concept (full of work) Jetpack App Startup How to use and principle analysis 5.Jetpack comement-STRINGS IST Kit library 6 Source code analysis | ThreadedRenderer null pointer, the Choreographer meet 7, by the way. Source code analysis | events is how to the Activity? 9. Do not fall into the loop of the need to keep alive