android– 如何在选择textview时显示d出窗口而不是CAB?

android– 如何在选择textview时显示d出窗口而不是CAB?,第1张

概述我正在制作一个阅读应用程序,它有一个全屏活动.当用户选择文本的一部分时,会出现一个带有复制选项的上下文 *** 作栏.这是默认行为.但是此 *** 作栏会阻止其下的文本,因此用户无法选择它.我想显示一个如下所示的d出窗口.我尝试从onCreateActionMode返回false,但是当我这样做时,我也无

我正在制作一个阅读应用程序,它有一个全屏活动.
当用户选择文本的一部分时,会出现一个带有复制选项的上下文 *** 作栏.这是默认行为.但是此 *** 作栏会阻止其下的文本,因此用户无法选择它.

我想显示一个如下所示的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.getSelectionStartSelection.getSelectionEnd找到.为方便起见,TextVIEw为我们提供了TextView.getSelectionStart,TextView.getSelectionEndTextView.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?所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1115190.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-29
下一篇 2022-05-29

发表评论

登录后才能评论

评论列表(0条)

保存