我正在制作一个阅读应用程序,它有一个全屏活动.
当用户选择文本的一部分时,会出现一个带有复制选项的上下文 *** 作栏.这是默认行为.但是此 *** 作栏会阻止其下的文本,因此用户无法选择它.
我想显示一个如下所示的d出窗口.
我尝试从onCreateActionMode返回false,但是当我这样做时,我也无法选择文本.
我想知道是否有一种标准的方法来实现这一点,因为许多阅读应用程序都使用这种设计.
解决方法:
我不知道Play Books是如何实现这一点的,但你可以创建一个PopupWindow
,并根据所选文本使用Layout.getSelectionPath
和一点点数学来计算它的位置.基本上,我们将:
>计算所选文本的边界
>计算PopupWindow的边界和初始位置
>计算两者之间的差异
>将PopupWindow偏移到水平/垂直居中于所选文本的上方或下方
计算选择范围
From the docs:
Fills in the specifIEd Path with a representation of a highlight
between the specifIEd offsets. This will often be a rectangle or a
potentially discontinuous set of rectangles. If the start and end are
the same, the returned path is empty.
因此,在我们的情况下,指定的偏移量将是选择的开始和结束,可以使用Selection.getSelectionStart
和Selection.getSelectionEnd
找到.为方便起见,TextVIEw为我们提供了TextView.getSelectionStart
,TextView.getSelectionEnd
和TextView.getLayout
.
final Path selDest = new Path(); final RectF selBounds = new RectF(); final Rect outBounds = new Rect(); // Calculate the selection start and end offset final int selStart = yourTextVIEw.getSelectionStart(); final int selEnd = yourTextVIEw.getSelectionEnd(); final int min = Math.max(0, Math.min(selStart, selEnd)); final int max = Math.max(0, Math.max(selStart, selEnd)); // Calculate the selection outBounds yourTextVIEw.getLayout().getSelectionPath(min, max, selDest); selDest.computeBounds(selBounds, true /* this param is ignored */); selBounds.roundOut(outBounds);
现在我们有了所选文本边界的Rect
,我们可以选择我们想要将PopupWindow放在哪里.在这种情况下,我们将沿着所选文本的顶部或底部水平居中,具体取决于我们显示d出窗口的空间大小.
计算初始d出坐标
接下来,我们需要计算d出内容的范围.要做到这一点,我们首先需要调用PopupWindow.showAtLocation
,但是我们膨胀的VIEw的界限不会立即可用,因此我建议使用ViewTreeObserver.OnGlobalLayoutListener
等待它们可用.
popupWindow.showAtLocation(yourTextVIEw, Gravity.top, 0, 0)
PopupWindow.showAtLocation需要:
>一个用于检索有效Window
token的视图,它只是唯一标识要放置d出窗口的窗口
>可选的重力,但在我们的例子中它将是Gravity.top
>可选的x / y偏移量
由于我们无法确定d出内容布局之前的x / y偏移量,因此我们最初将它放在默认位置.如果你尝试在传入的VIEw之前调用PopupWindow.showAtLocation,你会收到一个WindowManager.BadTokenException
,所以你可以考虑使用VIEwTreeObserver.OnGlobalLayoutListener来避免这种情况,但是当你选择了文本时它会出现.旋转你的设备.
final Rect cframe = new Rect(); final int[] cloc = new int[2]; popupContent.getLocationOnScreen(cloc); popupContent.getLocalVisibleRect(cbounds); popupContent.getwindowVisibledisplayFrame(cframe); final int scrollY = ((VIEw) yourTextVIEw.getParent()).getScrollY(); final int[] tloc = new int[2]; yourTextVIEw.getLocationInWindow(tloc); final int startX = cloc[0] + cbounds.centerX(); final int startY = cloc[1] + cbounds.centerY() - (tloc[1] - cframe.top) - scrollY;
> View.getLocationOnScreen
将返回d出内容的x / y坐标.
> View.getLocalVisibleRect
将返回d出内容的界限
> View.getWindowVisibleDisplayFrame
将返回偏移以适应 *** 作栏(如果存在)
> View.getScrollY
将返回我们TextVIEw所在的任何滚动容器的y偏移量(在我的情况下为ScrollVIEw)
> View.getLocationInWindow
将返回我们TextVIEw的y偏移量,以防 *** 作栏将其向下推一点
一旦我们获得了所需的所有信息,我们就可以计算出d出内容的最终起始x / y,然后使用它来计算它们与所选文本Rect之间的差异,这样我们就可以PopupWindow.update
到新位置.
计算偏移d出坐标
// Calculate the top and bottom offset of the popup relative to the selection bounds final int popupHeight = cbounds.height(); final int textpadding = yourTextVIEw.getpaddingleft(); final int topOffset = Math.round(selBounds.top - startY); final int btmOffset = Math.round(selBounds.bottom - (startY - popupHeight)); // Calculate the x/y coordinates for the popup relative to the selection bounds final int x = Math.round(selBounds.centerX() + textpadding - startX); final int y = Math.round(selBounds.top - scrollY < startY ? btmOffset : topOffset);
如果有足够的空间在所选文本上方显示d出窗口,我们会把它放在那里;否则,我们会将其偏移到所选文本下方.在我的情况下,我在TextVIEw周围有16dp填充,因此也需要考虑.我们最终将得到最终的x和y位置以抵消PopupWindow.
popupWindow.update(x, y, -1, -1);
-1这里只代表我们为PopupWindow提供的默认宽度/高度,在我们的例子中它将是ViewGroup.LayoutParams.WRAP_CONTENT
倾听选择的变化
我们希望PopupWindow每次更改所选文本时都会更新.
监听选择更改的一种简单方法是将TextVIEw子类化并提供对TextView.onSelectionChanged
的回调.
public class NotifyingSelectionTextVIEw extends AppCompatTextVIEw { private Selectionchangelistener Listener; public NotifyingSelectionTextVIEw(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @OverrIDe protected voID onSelectionChanged(int selStart, int selEnd) { super.onSelectionChanged(selStart, selEnd); if (Listener != null) { if (hasSelection()) { Listener.onTextSelected(); } else { Listener.onTextUnselected(); } } } public voID setSelectionchangelistener(Selectionchangelistener Listener) { this.Listener = Listener; } public interface Selectionchangelistener { voID onTextSelected(); voID onTextUnselected(); }}
听滚动更改
如果在ScrollVIEw等滚动容器中有TextVIEw,您可能还希望监听滚动更改,以便在滚动时锚定d出窗口.一种简单的方法是监听ScrollVIEw,并提供对View.onScrollChanged
的回调
public class NotifyingScrollVIEw extends ScrollVIEw { private Scrollchangelistener Listener; public NotifyingScrollVIEw(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @OverrIDe protected voID onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (Listener != null) { Listener.onScrollChanged(); } } public voID setScrollchangelistener(Scrollchangelistener Listener) { this.Listener = Listener; } public interface Scrollchangelistener { voID onScrollChanged(); }}
创建一个空的ActionMode.Callback
就像你在帖子中提到的那样,我们需要在ActionMode.Callback.onCreateActionMode
中返回true,以便我们的文本保持可选.但是我们还需要在@L_301_25@中调用Menu.clear
,以便删除在ActionMode中为所选文本找到的所有项目.
/** An {@link ActionMode.Callback} used to remove all action items from text selection */static final class EmptyActionMode extends SimpleActionModeCallback { @OverrIDe public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Return true to ensure the text is still selectable return true; } @OverrIDe public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // Remove all action items to provIDe an actionmode-less selection menu.clear(); return true; }}
现在我们可以使用TextView.setCustomSelectionActionModeCallback
来应用我们的自定义ActionMode. SimpleActionModeCallback是一个自定义类,只为ActionMode.Callback提供存根,有点类似于ViewPager.SimpleOnPageChangeListener
public class SimpleActionModeCallback implements ActionMode.Callback { @OverrIDe public boolean onCreateActionMode(ActionMode mode, Menu menu) { return false; } @OverrIDe public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @OverrIDe public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return false; } @OverrIDe public voID onDestroyActionMode(ActionMode mode) { }}
布局
这是我们正在使用的Activity布局:
<your.package.name.NotifyingScrollVIEw xmlns:androID="http://schemas.androID.com/apk/res/androID" androID:ID="@+ID/notifying_scroll_vIEw" androID:layout_wIDth="match_parent" androID:layout_height="match_parent"> <your.package.name.NotifyingSelectionTextVIEw androID:ID="@+ID/notifying_text_vIEw" androID:layout_wIDth="wrap_content" androID:layout_height="wrap_content" androID:padding="16dp" androID:textIsSelectable="true" androID:textSize="20sp" /></your.package.name.NotifyingScrollVIEw>
这是我们的d出布局:
<linearLayout xmlns:androID="http://schemas.androID.com/apk/res/androID" xmlns:tools="http://schemas.androID.com/tools" androID:layout_wIDth="wrap_content" androID:layout_height="wrap_content" androID:background="@drawable/action_mode_popup_bg" androID:orIEntation="vertical" tools:ignore="ContentDescription"> <linearLayout androID:layout_wIDth="match_parent" androID:layout_height="wrap_content" androID:orIEntation="horizontal"> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_add_note" androID:src="@drawable/ic_note_add_black_24dp" /> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_translate" androID:src="@drawable/ic_translate_black_24dp" /> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_search" androID:src="@drawable/ic_search_black_24dp" /> </linearLayout> <VIEw androID:layout_wIDth="match_parent" androID:layout_height="1dp" androID:layout_margin="8dp" androID:background="@androID:color/darker_gray" /> <linearLayout androID:layout_wIDth="wrap_content" androID:layout_height="wrap_content" androID:orIEntation="horizontal"> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_red" androID:src="@drawable/round_red" /> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_yellow" androID:src="@drawable/round_yellow" /> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_green" androID:src="@drawable/round_green" /> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_blue" androID:src="@drawable/round_blue" /> <Imagebutton androID:ID="@+ID/vIEw_action_mode_popup_clear_format" androID:src="@drawable/ic_format_clear_black_24dp" androID:visibility="gone" /> </linearLayout></linearLayout>
这些是我们的d出按钮样式:
<style name="ActionModePopupbutton"> <item name="androID:layout_wIDth">48dp</item> <item name="androID:layout_height">48dp</item> <item name="androID:layout_weight">1</item> <item name="androID:background">?selectableItemBackground</item></style><style name="ActionModePopupSwatch" parent="ActionModePopupbutton"> <item name="androID:padding">12dp</item></style>
UTIL
您将看到的VIEwUtils.onGlobalLayout只是一个用于处理某些VIEwTreeObserver.OnGlobalLayoutListener样板的util方法.
public static voID onGlobalLayout(final VIEw vIEw, final Runnable runnable) { final OnGlobalLayoutListener Listener = new OnGlobalLayoutListener() { @OverrIDe public voID onGlobalLayout() { vIEw.getVIEwTreeObserver().removeOnGlobalLayoutListener(this); runnable.run(); } }; vIEw.getVIEwTreeObserver().addOnGlobalLayoutListener(Listener);}
完全带来它
那么,现在我们已经:
>计算选定的文本范围
>计算d出范围
>计算差异并确定d出偏移量
>提供了一种监听滚动更改和选择更改的方法
>创建了我们的活动和d出布局
把所有东西放在一起可能看起来像:
public class ActionModePopupActivity extends AppCompatActivity implements Scrollchangelistener, Selectionchangelistener { private static final int DEFAulT_WIDTH = -1; private static final int DEFAulT_HEIGHT = -1; private final Point currLoc = new Point(); private final Point startLoc = new Point(); private final Rect cbounds = new Rect(); private final PopupWindow popupWindow = new PopupWindow(); private final ActionMode.Callback emptyActionMode = new EmptyActionMode(); private NotifyingSelectionTextVIEw yourTextVIEw; @Suppresslint("InflateParams") @OverrIDe protected voID onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentVIEw(R.layout.activity_action_mode_popup); // Initialize the popup content, only add it to the Window once we've selected text final LayoutInflater inflater = LayoutInflater.from(this); popupWindow.setContentVIEw(inflater.inflate(R.layout.vIEw_action_mode_popup, null)); popupWindow.setWIDth(WRAP_CONTENT); popupWindow.setHeight(WRAP_CONTENT); // Initialize to the NotifyingScrollVIEw to observe scroll changes final NotifyingScrollVIEw scroll = (NotifyingScrollVIEw) findVIEwByID(R.ID.notifying_scroll_vIEw); scroll.setScrollchangelistener(this); // Initialize the TextVIEw to observe selection changes and provIDe an empty ActionMode yourTextVIEw = (NotifyingSelectionTextVIEw) findVIEwByID(R.ID.notifying_text_vIEw); yourTextVIEw.setText(IPSUM); yourTextVIEw.setSelectionchangelistener(this); yourTextVIEw.setCustomSelectionActionModeCallback(emptyActionMode); } @OverrIDe public voID onScrollChanged() { // Anchor the popup while the user scrolls if (popupWindow.isShowing()) { final Point ploc = calculatePopupLocation(); popupWindow.update(ploc.x, ploc.y, DEFAulT_WIDTH, DEFAulT_HEIGHT); } } @OverrIDe public voID onTextSelected() { final VIEw popupContent = popupWindow.getContentVIEw(); if (popupWindow.isShowing()) { // Calculate the updated x/y pop coordinates final Point ploc = calculatePopupLocation(); popupWindow.update(ploc.x, ploc.y, DEFAulT_WIDTH, DEFAulT_HEIGHT); } else { // Add the popup to the Window and position it relative to the selected text bounds VIEwUtils.onGlobalLayout(yourTextVIEw, () -> { popupWindow.showAtLocation(yourTextVIEw, top, 0, 0); // Wait for the popup content to be laID out VIEwUtils.onGlobalLayout(popupContent, () -> { final Rect cframe = new Rect(); final int[] cloc = new int[2]; popupContent.getLocationOnScreen(cloc); popupContent.getLocalVisibleRect(cbounds); popupContent.getwindowVisibledisplayFrame(cframe); final int scrollY = ((VIEw) yourTextVIEw.getParent()).getScrollY(); final int[] tloc = new int[2]; yourTextVIEw.getLocationInWindow(tloc); final int startX = cloc[0] + cbounds.centerX(); final int startY = cloc[1] + cbounds.centerY() - (tloc[1] - cframe.top) - scrollY; startLoc.set(startX, startY); final Point ploc = calculatePopupLocation(); popupWindow.update(ploc.x, ploc.y, DEFAulT_WIDTH, DEFAulT_HEIGHT); }); }); } } @OverrIDe public voID onTextUnselected() { popupWindow.dismiss(); } /** Used to calculate where we should position the {@link PopupWindow} */ private Point calculatePopupLocation() { final ScrollVIEw parent = (ScrollVIEw) yourTextVIEw.getParent(); // Calculate the selection start and end offset final int selStart = yourTextVIEw.getSelectionStart(); final int selEnd = yourTextVIEw.getSelectionEnd(); final int min = Math.max(0, Math.min(selStart, selEnd)); final int max = Math.max(0, Math.max(selStart, selEnd)); // Calculate the selection bounds final RectF selBounds = new RectF(); final Path selection = new Path(); yourTextVIEw.getLayout().getSelectionPath(min, max, selection); selection.computeBounds(selBounds, true /* this param is ignored */); // RetrIEve the center x/y of the popup content final int cx = startLoc.x; final int cy = startLoc.y; // Calculate the top and bottom offset of the popup relative to the selection bounds final int popupHeight = cbounds.height(); final int textpadding = yourTextVIEw.getpaddingleft(); final int topOffset = Math.round(selBounds.top - cy); final int btmOffset = Math.round(selBounds.bottom - (cy - popupHeight)); // Calculate the x/y coordinates for the popup relative to the selection bounds final int scrollY = parent.getScrollY(); final int x = Math.round(selBounds.centerX() + textpadding - cx); final int y = Math.round(selBounds.top - scrollY < cy ? btmOffset : topOffset); currLoc.set(x, y - scrollY); return currLoc; } /** An {@link ActionMode.Callback} used to remove all action items from text selection */ static final class EmptyActionMode extends SimpleActionModeCallback { @OverrIDe public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Return true to ensure the yourTextVIEw is still selectable return true; } @OverrIDe public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // Remove all action items to provIDe an actionmode-less selection menu.clear(); return true; } }}
结果
With the action bar (link to video):
Without the action bar (link to video):
奖金 – 动画
因为我们知道PopupWindow的起始位置和偏移位置随着选择的变化而变化,所以我们可以轻松地在两个值之间执行线性插值,以便在我们移动物体时创建一个漂亮的动画.
public static float lerp(float a, float b, float v) { return a + (b - a) * v;}
private static final int DEFAulT_ANIM_DUR = 350;private static final int DEFAulT_ANIM_DELAY = 500;@OverrIDepublic voID onTextSelected() { final VIEw popupContent = popupWindow.getContentVIEw(); if (popupWindow.isShowing()) { // Calculate the updated x/y pop coordinates popupContent.getHandler().removeCallbacksAndMessages(null); popupContent.postDelayed(() -> { // The current x/y location of the popup final int currx = currLoc.x; final int curry = currLoc.y; // Calculate the updated x/y pop coordinates final Point ploc = calculatePopupLocation(); currLoc.set(ploc.x, ploc.y); // linear interpolate between the current and updated popup coordinates final ValueAnimator anim = ValueAnimator.offloat(0f, 1f); anim.addUpdateListener(animation -> { final float v = (float) animation.getAnimatedValue(); final int x = Math.round(Animutils.lerp(currx, ploc.x, v)); final int y = Math.round(Animutils.lerp(curry, ploc.y, v)); popupWindow.update(x, y, DEFAulT_WIDTH, DEFAulT_HEIGHT); }); anim.setDuration(DEFAulT_ANIM_DUR); anim.start(); }, DEFAulT_ANIM_DELAY); } else { ... }}
结果
With the action bar – animation (link to video)
额外
我没有讨论如何将单击侦听器附加到d出 *** 作上,并且可能有多种方法可以通过不同的计算和实现来实现相同的效果.但我要提一下,如果你想检索所选文本,然后用它做一些事情,you’d just need to CharSequence.subSequence
the min
and max
from the selected text.
无论如何,我希望这对你有所帮助!如果您有任何疑问,请告诉我.
总结以上是内存溢出为你收集整理的android – 如何在选择textview时显示d出窗口而不是CAB?全部内容,希望文章能够帮你解决android – 如何在选择textview时显示d出窗口而不是CAB?所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)