Loading core/api/current.txt +6 −0 Original line number Diff line number Diff line Loading @@ -636,6 +636,7 @@ package android { field public static final int ellipsize = 16842923; // 0x10100ab field public static final int ems = 16843096; // 0x1010158 field public static final int enableOnBackInvokedCallback = 16844396; // 0x101066c field public static final int enableTextStylingShortcuts; field public static final int enableVrMode = 16844069; // 0x1010525 field public static final int enabled = 16842766; // 0x101000e field public static final int end = 16843996; // 0x10104dc Loading Loading @@ -2113,6 +2114,7 @@ package android { field public static final int addToDictionary = 16908330; // 0x102002a field public static final int autofill = 16908355; // 0x1020043 field public static final int background = 16908288; // 0x1020000 field public static final int bold; field public static final int button1 = 16908313; // 0x1020019 field public static final int button2 = 16908314; // 0x102001a field public static final int button3 = 16908315; // 0x102001b Loading @@ -2138,6 +2140,7 @@ package android { field public static final int inputExtractAccessories = 16908378; // 0x102005a field public static final int inputExtractAction = 16908377; // 0x1020059 field public static final int inputExtractEditText = 16908325; // 0x1020025 field public static final int italic; field @Deprecated public static final int keyboardView = 16908326; // 0x1020026 field public static final int list = 16908298; // 0x102000a field public static final int list_container = 16908351; // 0x102003f Loading Loading @@ -2169,6 +2172,7 @@ package android { field public static final int textAssist = 16908353; // 0x1020041 field public static final int title = 16908310; // 0x1020016 field public static final int toggle = 16908311; // 0x1020017 field public static final int underline; field public static final int undo = 16908338; // 0x1020032 field public static final int widget_frame = 16908312; // 0x1020018 } Loading Loading @@ -55995,9 +55999,11 @@ package android.widget { ctor public EditText(android.content.Context, android.util.AttributeSet, int, int); method public void extendSelection(int); method public android.text.Editable getText(); method public boolean isStyleShortcutEnabled(); method public void selectAll(); method public void setSelection(int, int); method public void setSelection(int); method public void setStyleShortcutsEnabled(boolean); } public interface ExpandableListAdapter { core/java/android/text/style/SpanUtils.java 0 → 100644 +351 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.text.style; import android.annotation.IntRange; import android.annotation.NonNull; import android.graphics.Typeface; import android.text.Spannable; import android.text.Spanned; import android.util.LongArray; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @hide */ public class SpanUtils { private SpanUtils() {} // Do not instantiate /** * Toggle the bold state of the given range. * * If there is at least one character is not bold in the given range, make the entire region to * be bold. If all characters of the given range is already bolded, this method removes bold * style from the given selection. * * @param spannable a spannable string * @param min minimum inclusive index of the selection. * @param max maximum exclusive index of the selection. * @return true if the selected region is toggled. */ public static boolean toggleBold(@NonNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class); final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>(); for (StyleSpan span : boldSpans) { if ((span.getStyle() & Typeface.BOLD) == Typeface.BOLD) { filteredBoldSpans.add(span); } } if (!isCovered(spannable, filteredBoldSpans, min, max)) { // At least one character doesn't have bold style. Making given region bold. spannable.setSpan( new StyleSpan(Typeface.BOLD), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return true; } // Span covers the entire selection. Removing spans from tha region. for (int si = 0; si < filteredBoldSpans.size(); ++si) { final StyleSpan span = filteredBoldSpans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); final int flag = spannable.getSpanFlags(span); // If BOLD_ITALIC style is attached, need to set ITALIC span to the subtracted range. final boolean needItalicSpan = (span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC; if (start < min) { if (end > max) { // selection: ------------|===================|---------------- // span: <--------------------------------> // result: <-------> <----> spannable.setSpan(span, start, min, flag); spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, max, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: <-------> spannable.setSpan(span, start, min, flag); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, end, flag); } } } else { if (end > max) { // selection: ------------|===================|---------------- // span: <------------------------> // result: <------------> spannable.setSpan(span, max, end, flag); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), max, end, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: spannable.removeSpan(span); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag); } } } } return true; } /** * Toggle the italic state of the given range. * * If there is at least one character is not italic in the given range, make the entire region * to be italic. If all characters of the given range is already italic, this method removes * italic style from the given selection. * * @param spannable a spannable string * @param min minimum inclusive index of the selection. * @param max maximum exclusive index of the selection. * @return true if the selected region is toggled. */ public static boolean toggleItalic(@NonNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class); final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>(); for (StyleSpan span : boldSpans) { if ((span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC) { filteredBoldSpans.add(span); } } if (!isCovered(spannable, filteredBoldSpans, min, max)) { // At least one character doesn't have italic style. Making given region italic. spannable.setSpan( new StyleSpan(Typeface.ITALIC), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return true; } // Span covers the entire selection. Removing spans from tha region. for (int si = 0; si < filteredBoldSpans.size(); ++si) { final StyleSpan span = filteredBoldSpans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); final int flag = spannable.getSpanFlags(span); // If BOLD_ITALIC style is attached, need to set BOLD span to the subtracted range. final boolean needBoldSpan = (span.getStyle() & Typeface.BOLD) == Typeface.BOLD; if (start < min) { if (end > max) { // selection: ------------|===================|---------------- // span: <--------------------------------> // result: <-------> <----> spannable.setSpan(span, start, min, flag); spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), min, max, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: <-------> spannable.setSpan(span, start, min, flag); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), min, end, flag); } } } else { if (end > max) { // selection: ------------|===================|---------------- // span: <------------------------> // result: <------------> spannable.setSpan(span, max, end, flag); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), max, end, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: spannable.removeSpan(span); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag); } } } } return true; } /** * Toggle the underline state of the given range. * * If there is at least one character is not underlined in the given range, make the entire * region to underlined. If all characters of the given range is already underlined, this * method removes underline from the given selection. * * @param spannable a spannable string * @param min minimum inclusive index of the selection. * @param max maximum exclusive index of the selection. * @return true if the selected region is toggled. */ public static boolean toggleUnderline(@NonNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } final List<UnderlineSpan> spans = Arrays.asList(spannable.getSpans(min, max, UnderlineSpan.class)); if (!isCovered(spannable, spans, min, max)) { // At least one character doesn't have underline style. Making given region underline. spannable.setSpan(new UnderlineSpan(), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return true; } // Span covers the entire selection. Removing spans from tha region. for (int si = 0; si < spans.size(); ++si) { final UnderlineSpan span = spans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); final int flag = spannable.getSpanFlags(span); if (start < min) { if (end > max) { // selection: ------------|===================|---------------- // span: <--------------------------------> // result: <-------> <----> spannable.setSpan(span, start, min, flag); spannable.setSpan(new UnderlineSpan(), max, end, flag); } else { // selection: ------------|===================|---------------- // span: <-----------> // result: <-------> spannable.setSpan(span, start, min, flag); } } else { if (end > max) { // selection: ------------|===================|---------------- // span: <------------------------> // result: <------------> spannable.setSpan(span, max, end, flag); } else { // selection: ------------|===================|---------------- // span: <-----------> // result: spannable.removeSpan(span); } } } return true; } private static long pack(int from, int to) { return ((long) from) << 32 | (long) to; } private static int min(long packed) { return (int) (packed >> 32); } private static int max(long packed) { return (int) (packed & 0xFFFFFFFFL); } private static boolean hasIntersection(int aMin, int aMax, int bMin, int bMax) { return aMin < bMax && bMin < aMax; } private static long intersection(int aMin, int aMax, int bMin, int bMax) { return pack(Math.max(aMin, bMin), Math.min(aMax, bMax)); } private static <T> boolean isCovered(@NonNull Spannable spannable, @NonNull List<T> spans, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } LongArray uncoveredRanges = new LongArray(); LongArray nextUncoveredRanges = new LongArray(); uncoveredRanges.add(pack(min, max)); for (int si = 0; si < spans.size(); ++si) { final T span = spans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); for (int i = 0; i < uncoveredRanges.size(); ++i) { final long packed = uncoveredRanges.get(i); final int uncoveredStart = min(packed); final int uncoveredEnd = max(packed); if (!hasIntersection(start, end, uncoveredStart, uncoveredEnd)) { // This span doesn't affect this uncovered range. Try next span. nextUncoveredRanges.add(packed); } else { // This span has an intersection with uncovered range. Update the uncovered // range. long intersectionPack = intersection(start, end, uncoveredStart, uncoveredEnd); int intersectStart = min(intersectionPack); int intersectEnd = max(intersectionPack); // Uncovered Range : ----------|=======================|------------- // Intersection : <----------> // Remaining uncovered ranges: ----------|=====|----------|======|------------- if (uncoveredStart != intersectStart) { // There is still uncovered area on the left. nextUncoveredRanges.add(pack(uncoveredStart, intersectStart)); } if (intersectEnd != uncoveredEnd) { // There is still uncovered area on the right. nextUncoveredRanges.add(pack(intersectEnd, uncoveredEnd)); } } } if (nextUncoveredRanges.size() == 0) { return true; } // Swap the uncoveredRanges and nextUncoveredRanges and clear the next one. final LongArray tmp = nextUncoveredRanges; nextUncoveredRanges = uncoveredRanges; uncoveredRanges = tmp; nextUncoveredRanges.clear(); } return false; } } core/java/android/widget/EditText.java +101 −0 Original line number Diff line number Diff line Loading @@ -17,13 +17,17 @@ package android.widget; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.text.Editable; import android.text.Selection; import android.text.Spannable; import android.text.TextUtils; import android.text.method.ArrowKeyMovementMethod; import android.text.method.MovementMethod; import android.text.style.SpanUtils; import android.util.AttributeSet; import android.view.KeyEvent; /* * This is supposed to be a *very* thin veneer over TextView. Loading Loading @@ -69,8 +73,18 @@ import android.util.AttributeSet; * See {@link android.R.styleable#EditText EditText Attributes}, * {@link android.R.styleable#TextView TextView Attributes}, * {@link android.R.styleable#View View Attributes} * * @attr ref android.R.styleable#EditText_enableTextStylingShortcuts */ public class EditText extends TextView { // True if the style shortcut is enabled. private boolean mStyleShortcutsEnabled = false; private static final int ID_BOLD = android.R.id.bold; private static final int ID_ITALIC = android.R.id.italic; private static final int ID_UNDERLINE = android.R.id.underline; public EditText(Context context) { this(context, null); } Loading @@ -85,6 +99,20 @@ public class EditText extends TextView { public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final Resources.Theme theme = context.getTheme(); final TypedArray a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes); final int n = a.getIndexCount(); for (int i = 0; i < n; ++i) { int attr = a.getIndex(i); switch (attr) { case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts: mStyleShortcutsEnabled = a.getBoolean(attr, false); break; } } } @Override Loading Loading @@ -178,4 +206,77 @@ public class EditText extends TextView { protected boolean supportsAutoSizeText() { return false; } @Override public boolean onKeyShortcut(int keyCode, KeyEvent event) { if (event.hasModifiers(KeyEvent.META_CTRL_ON)) { // Handle Ctrl-only shortcuts. switch (keyCode) { case KeyEvent.KEYCODE_B: if (mStyleShortcutsEnabled && hasSelection()) { return onTextContextMenuItem(ID_BOLD); } break; case KeyEvent.KEYCODE_I: if (mStyleShortcutsEnabled && hasSelection()) { return onTextContextMenuItem(ID_ITALIC); } break; case KeyEvent.KEYCODE_U: if (mStyleShortcutsEnabled && hasSelection()) { return onTextContextMenuItem(ID_UNDERLINE); } break; } } return super.onKeyShortcut(keyCode, event); } @Override public boolean onTextContextMenuItem(int id) { // TODO: Move to switch-case once the resource ID is finalized. if (id == ID_BOLD || id == ID_ITALIC || id == ID_UNDERLINE) { return performStylingAction(id); } return super.onTextContextMenuItem(id); } private boolean performStylingAction(int actionId) { final int selectionStart = getSelectionStart(); final int selectionEnd = getSelectionEnd(); if (selectionStart < 0 || selectionEnd < 0) { return false; // There is no selection. } int min = Math.min(selectionStart, selectionEnd); int max = Math.max(selectionStart, selectionEnd); Spannable spannable = getText(); if (actionId == ID_BOLD) { return SpanUtils.toggleBold(spannable, min, max); } else if (actionId == ID_ITALIC) { return SpanUtils.toggleItalic(spannable, min, max); } else if (actionId == ID_UNDERLINE) { return SpanUtils.toggleUnderline(spannable, min, max); } return false; } /** * Enables styls shortcuts, e.g. Ctrl+B for making text bold. * * @param enabled true for enabled, false for disabled. */ public void setStyleShortcutsEnabled(boolean enabled) { mStyleShortcutsEnabled = enabled; } /** * Return true if style shortcut is enabled, otherwise returns false. * @return true if style shortcut is enabled, otherwise returns false. */ public boolean isStyleShortcutEnabled() { return mStyleShortcutsEnabled; } } core/res/res/values/attrs.xml +2 −0 Original line number Diff line number Diff line Loading @@ -4509,6 +4509,8 @@ </attr> </declare-styleable> <declare-styleable name="EditText"> <!-- Enables styling shortcuts, e.g. Ctrl+B for bold. This is off by default. --> <attr name="enableTextStylingShortcuts" format="boolean" /> </declare-styleable> <declare-styleable name="FastScroll"> <!-- Drawable used for the scroll bar thumb. --> Loading core/res/res/values/ids.xml +6 −0 Original line number Diff line number Diff line Loading @@ -70,6 +70,12 @@ <item type="id" name="cut" /> <item type="id" name="copy" /> <item type="id" name="paste" /> <!-- Editor action that makes selected text bold. --> <item type="id" name="bold" /> <!-- Editor action that makes selected text italic. --> <item type="id" name="italic" /> <!-- Editor action that makes selected text underline. --> <item type="id" name="underline" /> <item type="id" name="copyUrl" /> <item type="id" name="selectTextMode" /> <item type="id" name="switchInputMethod" /> Loading Loading
core/api/current.txt +6 −0 Original line number Diff line number Diff line Loading @@ -636,6 +636,7 @@ package android { field public static final int ellipsize = 16842923; // 0x10100ab field public static final int ems = 16843096; // 0x1010158 field public static final int enableOnBackInvokedCallback = 16844396; // 0x101066c field public static final int enableTextStylingShortcuts; field public static final int enableVrMode = 16844069; // 0x1010525 field public static final int enabled = 16842766; // 0x101000e field public static final int end = 16843996; // 0x10104dc Loading Loading @@ -2113,6 +2114,7 @@ package android { field public static final int addToDictionary = 16908330; // 0x102002a field public static final int autofill = 16908355; // 0x1020043 field public static final int background = 16908288; // 0x1020000 field public static final int bold; field public static final int button1 = 16908313; // 0x1020019 field public static final int button2 = 16908314; // 0x102001a field public static final int button3 = 16908315; // 0x102001b Loading @@ -2138,6 +2140,7 @@ package android { field public static final int inputExtractAccessories = 16908378; // 0x102005a field public static final int inputExtractAction = 16908377; // 0x1020059 field public static final int inputExtractEditText = 16908325; // 0x1020025 field public static final int italic; field @Deprecated public static final int keyboardView = 16908326; // 0x1020026 field public static final int list = 16908298; // 0x102000a field public static final int list_container = 16908351; // 0x102003f Loading Loading @@ -2169,6 +2172,7 @@ package android { field public static final int textAssist = 16908353; // 0x1020041 field public static final int title = 16908310; // 0x1020016 field public static final int toggle = 16908311; // 0x1020017 field public static final int underline; field public static final int undo = 16908338; // 0x1020032 field public static final int widget_frame = 16908312; // 0x1020018 } Loading Loading @@ -55995,9 +55999,11 @@ package android.widget { ctor public EditText(android.content.Context, android.util.AttributeSet, int, int); method public void extendSelection(int); method public android.text.Editable getText(); method public boolean isStyleShortcutEnabled(); method public void selectAll(); method public void setSelection(int, int); method public void setSelection(int); method public void setStyleShortcutsEnabled(boolean); } public interface ExpandableListAdapter {
core/java/android/text/style/SpanUtils.java 0 → 100644 +351 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.text.style; import android.annotation.IntRange; import android.annotation.NonNull; import android.graphics.Typeface; import android.text.Spannable; import android.text.Spanned; import android.util.LongArray; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @hide */ public class SpanUtils { private SpanUtils() {} // Do not instantiate /** * Toggle the bold state of the given range. * * If there is at least one character is not bold in the given range, make the entire region to * be bold. If all characters of the given range is already bolded, this method removes bold * style from the given selection. * * @param spannable a spannable string * @param min minimum inclusive index of the selection. * @param max maximum exclusive index of the selection. * @return true if the selected region is toggled. */ public static boolean toggleBold(@NonNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class); final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>(); for (StyleSpan span : boldSpans) { if ((span.getStyle() & Typeface.BOLD) == Typeface.BOLD) { filteredBoldSpans.add(span); } } if (!isCovered(spannable, filteredBoldSpans, min, max)) { // At least one character doesn't have bold style. Making given region bold. spannable.setSpan( new StyleSpan(Typeface.BOLD), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return true; } // Span covers the entire selection. Removing spans from tha region. for (int si = 0; si < filteredBoldSpans.size(); ++si) { final StyleSpan span = filteredBoldSpans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); final int flag = spannable.getSpanFlags(span); // If BOLD_ITALIC style is attached, need to set ITALIC span to the subtracted range. final boolean needItalicSpan = (span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC; if (start < min) { if (end > max) { // selection: ------------|===================|---------------- // span: <--------------------------------> // result: <-------> <----> spannable.setSpan(span, start, min, flag); spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, max, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: <-------> spannable.setSpan(span, start, min, flag); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, end, flag); } } } else { if (end > max) { // selection: ------------|===================|---------------- // span: <------------------------> // result: <------------> spannable.setSpan(span, max, end, flag); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), max, end, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: spannable.removeSpan(span); if (needItalicSpan) { spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag); } } } } return true; } /** * Toggle the italic state of the given range. * * If there is at least one character is not italic in the given range, make the entire region * to be italic. If all characters of the given range is already italic, this method removes * italic style from the given selection. * * @param spannable a spannable string * @param min minimum inclusive index of the selection. * @param max maximum exclusive index of the selection. * @return true if the selected region is toggled. */ public static boolean toggleItalic(@NonNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class); final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>(); for (StyleSpan span : boldSpans) { if ((span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC) { filteredBoldSpans.add(span); } } if (!isCovered(spannable, filteredBoldSpans, min, max)) { // At least one character doesn't have italic style. Making given region italic. spannable.setSpan( new StyleSpan(Typeface.ITALIC), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return true; } // Span covers the entire selection. Removing spans from tha region. for (int si = 0; si < filteredBoldSpans.size(); ++si) { final StyleSpan span = filteredBoldSpans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); final int flag = spannable.getSpanFlags(span); // If BOLD_ITALIC style is attached, need to set BOLD span to the subtracted range. final boolean needBoldSpan = (span.getStyle() & Typeface.BOLD) == Typeface.BOLD; if (start < min) { if (end > max) { // selection: ------------|===================|---------------- // span: <--------------------------------> // result: <-------> <----> spannable.setSpan(span, start, min, flag); spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), min, max, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: <-------> spannable.setSpan(span, start, min, flag); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), min, end, flag); } } } else { if (end > max) { // selection: ------------|===================|---------------- // span: <------------------------> // result: <------------> spannable.setSpan(span, max, end, flag); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), max, end, flag); } } else { // selection: ------------|===================|---------------- // span: <-----------> // result: spannable.removeSpan(span); if (needBoldSpan) { spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag); } } } } return true; } /** * Toggle the underline state of the given range. * * If there is at least one character is not underlined in the given range, make the entire * region to underlined. If all characters of the given range is already underlined, this * method removes underline from the given selection. * * @param spannable a spannable string * @param min minimum inclusive index of the selection. * @param max maximum exclusive index of the selection. * @return true if the selected region is toggled. */ public static boolean toggleUnderline(@NonNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } final List<UnderlineSpan> spans = Arrays.asList(spannable.getSpans(min, max, UnderlineSpan.class)); if (!isCovered(spannable, spans, min, max)) { // At least one character doesn't have underline style. Making given region underline. spannable.setSpan(new UnderlineSpan(), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return true; } // Span covers the entire selection. Removing spans from tha region. for (int si = 0; si < spans.size(); ++si) { final UnderlineSpan span = spans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); final int flag = spannable.getSpanFlags(span); if (start < min) { if (end > max) { // selection: ------------|===================|---------------- // span: <--------------------------------> // result: <-------> <----> spannable.setSpan(span, start, min, flag); spannable.setSpan(new UnderlineSpan(), max, end, flag); } else { // selection: ------------|===================|---------------- // span: <-----------> // result: <-------> spannable.setSpan(span, start, min, flag); } } else { if (end > max) { // selection: ------------|===================|---------------- // span: <------------------------> // result: <------------> spannable.setSpan(span, max, end, flag); } else { // selection: ------------|===================|---------------- // span: <-----------> // result: spannable.removeSpan(span); } } } return true; } private static long pack(int from, int to) { return ((long) from) << 32 | (long) to; } private static int min(long packed) { return (int) (packed >> 32); } private static int max(long packed) { return (int) (packed & 0xFFFFFFFFL); } private static boolean hasIntersection(int aMin, int aMax, int bMin, int bMax) { return aMin < bMax && bMin < aMax; } private static long intersection(int aMin, int aMax, int bMin, int bMax) { return pack(Math.max(aMin, bMin), Math.min(aMax, bMax)); } private static <T> boolean isCovered(@NonNull Spannable spannable, @NonNull List<T> spans, @IntRange(from = 0) int min, @IntRange(from = 0) int max) { if (min == max) { return false; } LongArray uncoveredRanges = new LongArray(); LongArray nextUncoveredRanges = new LongArray(); uncoveredRanges.add(pack(min, max)); for (int si = 0; si < spans.size(); ++si) { final T span = spans.get(si); final int start = spannable.getSpanStart(span); final int end = spannable.getSpanEnd(span); for (int i = 0; i < uncoveredRanges.size(); ++i) { final long packed = uncoveredRanges.get(i); final int uncoveredStart = min(packed); final int uncoveredEnd = max(packed); if (!hasIntersection(start, end, uncoveredStart, uncoveredEnd)) { // This span doesn't affect this uncovered range. Try next span. nextUncoveredRanges.add(packed); } else { // This span has an intersection with uncovered range. Update the uncovered // range. long intersectionPack = intersection(start, end, uncoveredStart, uncoveredEnd); int intersectStart = min(intersectionPack); int intersectEnd = max(intersectionPack); // Uncovered Range : ----------|=======================|------------- // Intersection : <----------> // Remaining uncovered ranges: ----------|=====|----------|======|------------- if (uncoveredStart != intersectStart) { // There is still uncovered area on the left. nextUncoveredRanges.add(pack(uncoveredStart, intersectStart)); } if (intersectEnd != uncoveredEnd) { // There is still uncovered area on the right. nextUncoveredRanges.add(pack(intersectEnd, uncoveredEnd)); } } } if (nextUncoveredRanges.size() == 0) { return true; } // Swap the uncoveredRanges and nextUncoveredRanges and clear the next one. final LongArray tmp = nextUncoveredRanges; nextUncoveredRanges = uncoveredRanges; uncoveredRanges = tmp; nextUncoveredRanges.clear(); } return false; } }
core/java/android/widget/EditText.java +101 −0 Original line number Diff line number Diff line Loading @@ -17,13 +17,17 @@ package android.widget; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.text.Editable; import android.text.Selection; import android.text.Spannable; import android.text.TextUtils; import android.text.method.ArrowKeyMovementMethod; import android.text.method.MovementMethod; import android.text.style.SpanUtils; import android.util.AttributeSet; import android.view.KeyEvent; /* * This is supposed to be a *very* thin veneer over TextView. Loading Loading @@ -69,8 +73,18 @@ import android.util.AttributeSet; * See {@link android.R.styleable#EditText EditText Attributes}, * {@link android.R.styleable#TextView TextView Attributes}, * {@link android.R.styleable#View View Attributes} * * @attr ref android.R.styleable#EditText_enableTextStylingShortcuts */ public class EditText extends TextView { // True if the style shortcut is enabled. private boolean mStyleShortcutsEnabled = false; private static final int ID_BOLD = android.R.id.bold; private static final int ID_ITALIC = android.R.id.italic; private static final int ID_UNDERLINE = android.R.id.underline; public EditText(Context context) { this(context, null); } Loading @@ -85,6 +99,20 @@ public class EditText extends TextView { public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final Resources.Theme theme = context.getTheme(); final TypedArray a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes); final int n = a.getIndexCount(); for (int i = 0; i < n; ++i) { int attr = a.getIndex(i); switch (attr) { case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts: mStyleShortcutsEnabled = a.getBoolean(attr, false); break; } } } @Override Loading Loading @@ -178,4 +206,77 @@ public class EditText extends TextView { protected boolean supportsAutoSizeText() { return false; } @Override public boolean onKeyShortcut(int keyCode, KeyEvent event) { if (event.hasModifiers(KeyEvent.META_CTRL_ON)) { // Handle Ctrl-only shortcuts. switch (keyCode) { case KeyEvent.KEYCODE_B: if (mStyleShortcutsEnabled && hasSelection()) { return onTextContextMenuItem(ID_BOLD); } break; case KeyEvent.KEYCODE_I: if (mStyleShortcutsEnabled && hasSelection()) { return onTextContextMenuItem(ID_ITALIC); } break; case KeyEvent.KEYCODE_U: if (mStyleShortcutsEnabled && hasSelection()) { return onTextContextMenuItem(ID_UNDERLINE); } break; } } return super.onKeyShortcut(keyCode, event); } @Override public boolean onTextContextMenuItem(int id) { // TODO: Move to switch-case once the resource ID is finalized. if (id == ID_BOLD || id == ID_ITALIC || id == ID_UNDERLINE) { return performStylingAction(id); } return super.onTextContextMenuItem(id); } private boolean performStylingAction(int actionId) { final int selectionStart = getSelectionStart(); final int selectionEnd = getSelectionEnd(); if (selectionStart < 0 || selectionEnd < 0) { return false; // There is no selection. } int min = Math.min(selectionStart, selectionEnd); int max = Math.max(selectionStart, selectionEnd); Spannable spannable = getText(); if (actionId == ID_BOLD) { return SpanUtils.toggleBold(spannable, min, max); } else if (actionId == ID_ITALIC) { return SpanUtils.toggleItalic(spannable, min, max); } else if (actionId == ID_UNDERLINE) { return SpanUtils.toggleUnderline(spannable, min, max); } return false; } /** * Enables styls shortcuts, e.g. Ctrl+B for making text bold. * * @param enabled true for enabled, false for disabled. */ public void setStyleShortcutsEnabled(boolean enabled) { mStyleShortcutsEnabled = enabled; } /** * Return true if style shortcut is enabled, otherwise returns false. * @return true if style shortcut is enabled, otherwise returns false. */ public boolean isStyleShortcutEnabled() { return mStyleShortcutsEnabled; } }
core/res/res/values/attrs.xml +2 −0 Original line number Diff line number Diff line Loading @@ -4509,6 +4509,8 @@ </attr> </declare-styleable> <declare-styleable name="EditText"> <!-- Enables styling shortcuts, e.g. Ctrl+B for bold. This is off by default. --> <attr name="enableTextStylingShortcuts" format="boolean" /> </declare-styleable> <declare-styleable name="FastScroll"> <!-- Drawable used for the scroll bar thumb. --> Loading
core/res/res/values/ids.xml +6 −0 Original line number Diff line number Diff line Loading @@ -70,6 +70,12 @@ <item type="id" name="cut" /> <item type="id" name="copy" /> <item type="id" name="paste" /> <!-- Editor action that makes selected text bold. --> <item type="id" name="bold" /> <!-- Editor action that makes selected text italic. --> <item type="id" name="italic" /> <!-- Editor action that makes selected text underline. --> <item type="id" name="underline" /> <item type="id" name="copyUrl" /> <item type="id" name="selectTextMode" /> <item type="id" name="switchInputMethod" /> Loading