- android - 多次调用 OnPrimaryClipChangedListener
- android - 无法更新 RecyclerView 中的 TextView 字段
- android.database.CursorIndexOutOfBoundsException : Index 0 requested, 光标大小为 0
- android - 使用 AppCompat 时,我们是否需要明确指定其 UI 组件(Spinner、EditText)颜色
我正在制作一个阅读应用程序,它有一个全屏活动。
当用户选择文本的一部分时,会出现一个带有copy选项的contextual action bar
。这是默认行为。但是这个actionbar阻止了下面的文本,所以用户不能选择它。
我想显示一个弹出窗口如下。
我试图从false
返回onCreateActionMode
,但当我这样做时,我也无法选择文本。
我想知道是否有一个标准的方法来实现这一点,因为许多阅读应用程序使用这种设计。
最佳答案
我不知道play books是如何做到这一点的,但是你可以创建一个PopupWindow
并根据选择的文本使用Layout.getSelectionPath
和一点数学计算它的位置。基本上,我们要:
计算选定文本的边界
计算PopupWindow
的边界和初始位置
计算两者之间的差
将PopupWindow
偏移到所选文本上方或下方的水平/垂直静止中心
计算选择界限
From the docs
用高光的表示填充指定的路径
在指定的偏移之间。这通常是一个矩形或
可能不连续的矩形集。如果开始和结束是
同样,返回的路径也是空的。
因此,在我们的例子中,指定的偏移是选择的开始和结束,可以使用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
的位置。在这种情况下,我们将沿选定文本的顶部或底部水平居中,这取决于显示弹出窗口所需的空间。
PopupWindow.showAtLocation
,但我们充气的
View
的界限不会立即可用,因此我建议使用
ViewTreeObserver.OnGlobalLayoutListener
来等待它们可用。
popupWindow.showAtLocation(yourTextView, Gravity.TOP, 0, 0)
PopupWindow.showAtLocation
需要:
View
从中检索一个有效的
Window
token,它只是唯一地标识要放置弹出窗口的
Window
。
Gravity.TOP
PopupWindow.showAtLocation
设置好之前调用
View
,您将收到一个
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
将返回弹出内容的x/y坐标。
View.getLocalVisibleRect
将返回弹出内容的边界
View.getWindowVisibleDisplayFrame
将返回偏移量以适应操作栏(如果存在)
View.getScrollY
将返回我们的
y
所在的滚动容器的
TextView
偏移量(在我的例子中是
ScrollView
)
View.getLocationInWindow
将返回我们
y
的
TextView
偏移量,以防操作栏将其向下推一点
Rect
,这样我们就可以
PopupWindow.update
到新的位置。
// 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);
16dp
周围有
TextView
填充,所以这也需要考虑。我们将以最后的
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();
}
}
TextView
的滚动容器中有一个
ScrollView
,您可能还需要监听滚动更改,以便在滚动时锚定弹出窗口。监听这些消息的一个简单方法是对
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
true
中返回
ActionMode.Callback.onCreateActionMode
,以便我们的文本保持可选。但我们还需要在
Menu.clear
中调用
ActionMode.Callback.onPrepareActionMode
,以便删除您在
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>
<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"
style="@style/ActionModePopupButton"
android:src="@drawable/ic_note_add_black_24dp" />
<ImageButton
android:id="@+id/view_action_mode_popup_translate"
style="@style/ActionModePopupButton"
android:src="@drawable/ic_translate_black_24dp" />
<ImageButton
android:id="@+id/view_action_mode_popup_search"
style="@style/ActionModePopupButton"
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"
style="@style/ActionModePopupSwatch"
android:src="@drawable/round_red" />
<ImageButton
android:id="@+id/view_action_mode_popup_yellow"
style="@style/ActionModePopupSwatch"
android:src="@drawable/round_yellow" />
<ImageButton
android:id="@+id/view_action_mode_popup_green"
style="@style/ActionModePopupSwatch"
android:src="@drawable/round_green" />
<ImageButton
android:id="@+id/view_action_mode_popup_blue"
style="@style/ActionModePopupSwatch"
android:src="@drawable/round_blue" />
<ImageButton
android:id="@+id/view_action_mode_popup_clear_format"
style="@style/ActionModePopupSwatch"
android:src="@drawable/ic_format_clear_black_24dp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
<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>
ViewUtils.onGlobalLayout
只是一个处理一些
ViewTreeObserver.OnGlobalLayoutListener
样板文件的实用方法。
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);
}
Activity
和弹出式布局
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;
}
}
}
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;
@Override
public 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 {
...
}
}
CharSequence.subSequence
the min
and max
from the selected text。
关于android - 选择textview时如何显示弹出窗口而不是CAB?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43689020/
https://github.com/mattdiamond/Recorderjs/blob/master/recorder.js中的代码 我不明白 JavaScript 语法,比如 (functio
在 iOS 7 及更早版本中,如果我们想在应用程序中找到 topMostWindow,我们通常使用以下代码行 [[[UIApplication sharedApplication] windows]
我已经尝试解决这个问题很长一段时间了:我无法访问窗口的 url,因为它位于另一个域上..有一些解决方案吗? function login() { var cb = window.ope
是否可以将 FFMPEG 视频流传递到 C# 窗口?现在它在新窗口中作为新进程打开,我只是想将它传递给我自己的 SessionWindow。 此时我像这样执行ffplay: public void E
我有一个名为 x 的矩阵看起来像这样: pTime Close 1 1275087600 1.2268 2 1275264000 1.2264 3 1275264300 1.2
在编译时,发生搜索,grep搜索等,Emacs会在单独的窗口中创建一个新的缓冲区来显示结果,有没有自动跳转到那个窗口的方法?这很有用,因为我可以使用 n 和 p 而不是 M-g n 和 M-g p 移
我有一个启动 PowerShell 脚本的批处理文件。 批处理文件: START Powershell -executionpolicy RemoteSigned -noexit -file "MyS
我有一个基于菜单栏的应用程序,单击图标时会显示一个窗口。在 Mac OS X Lion 上一切正常,但由于某种原因,在 Snow Leopard 和早期版本的 Mac OS X 上会出现错误。任何时候
在 macOS 中,如何在 Xcode 和/或 Interface Builder 中创建带有“集成标题栏和工具栏”的窗口? 这是“宽标题栏”类型的窗口,已添加到 OS X 10.10 Yosemit
在浏览器 (Chrome) 中 JavaScript: var DataModler = { Data: { Something: 'value' }, Process: functi
我有 3 个 html 页面。第 1 页链接到第 2 页,第 2 页链接到第 3 页(为了简单起见)。 我希望页面 2 中的链接打开页面 3 并关闭页面 1(选项卡 1)。 据我了解,您无法使用 Ja
当点击“创建节点”按钮时,如何打开一个新的框架或窗口?我希望新框架包含一个文本字段和下拉菜单,以便用户可以选择一个选项。 Create node Search node
我有一个用户控件,用于编辑应用程序中的某些对象。 我最近遇到一个实例,我想弹出一个新的对话框(窗口)来托管此用户控件。 如何实例化新窗口并将需要设置的任何属性从窗口传递到用户控件? 感谢您的宝贵时间。
我有一个Observable,它发出许多对象,我想使用window或buffer操作对这些对象进行分组。但是,我不想指定count参数来确定窗口中应包含多少个对象,而是希望能够使用自定义条件。 例如,
我有以下代码,它打开一个新的 JavaFX 阶段(我们称之为窗口)。 openAlertBox.setOnAction(e -> { AlertBox alert = AlertBox
我要添加一个“在新窗口中打开”上下文菜单项,该菜单项将以新的UIScene打开我的应用程序文档之一。当然,我只想在实际上支持多个场景的设备上显示该菜单项。 目前,我只是在检查设备是否是使用旧设备的iP
我正在尝试创建一个 AIR 应用程序来记录应用程序的使用情况,使用 AIR 从系统获取信息的唯一简单方法是使用命令行工具和抓取 标准输出 . 我知道像 这样的工具顶部 和 ps 对于 OS X,但它们
所以我有这个简单的 turtle 螺旋制作器,我想知道是否有一种方法可以打印出由该程序创建的我的设计副本。 代码: import turtle x= float(input("Angle: ")) y
我正在编写一个 C# WPF 程序,它将文本消息发送到另一个程序的窗口。我有一个宏程序作为我的键盘驱动程序 (Logitech g15) 的一部分,它已经这样做了,尽管它不会将击键直接发送到进程,而是
我尝试使用以下代码通过 UDP 发送,但得到了奇怪的结果。 if((sendto(newSocket, sendBuf, totalLength, 0, (SOCKADDR *)&sendAd
我是一名优秀的程序员,十分优秀!