Android中的分页文本
Posted
技术标签:
【中文标题】Android中的分页文本【英文标题】:Paginating text in Android 【发布时间】:2015-10-28 13:03:12 【问题描述】:我正在为 android 编写一个简单的文本/电子书查看器,因此我使用了 TextView
向用户显示 html 格式的文本,以便他们可以通过来回浏览页面中的文本。但我的问题是我无法在 Android 中对文本进行分页。
我不能(或者我不知道如何)从TextView
用于将文本分成行和页的换行和分页算法获得适当的反馈。因此,我无法理解内容在实际显示中的结束位置,因此我从下一页的剩余部分继续。我想找到解决这个问题的方法。
如果我知道屏幕上最后绘制的字符是什么,我可以轻松地放置足够的字符来填满屏幕,并且知道实际绘制完成的位置,我可以继续下一页。这可能吗?怎么样?
在 *** 上已多次提出类似问题,但未提供令人满意的答案。这些只是其中的一部分:
How to paginate long text into pages in Android? Ebook reader pagination issue in android Paginate text based on rendered text size只有一个答案,似乎可行,但速度很慢。它添加字符和行,直到页面被填满。我不认为这是进行分页的好方法:
How to break styled text into pages in Android?除了这个问题,碰巧PageTurner eBook reader做的大部分是正确的,虽然它有点慢。
https://github.com/nightwhistler/pageturnerPS:我不局限于 TextView,而且我知道换行和分页算法可能非常复杂 (as in TeX),所以我不是在寻找最佳答案,而是在寻找一个相当快速的解决方案,可以可供用户使用。
更新:这似乎是获得正确答案的良好开端:
Is there a way of retrieving a TextView's visible line count or range?
答案: 完成文字排版后,可以找出可见的文字:
ViewTreeObserver vto = txtViewEx.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener()
@Override
public void onGlobalLayout()
ViewTreeObserver obs = txtViewEx.getViewTreeObserver();
obs.removeOnGlobalLayoutListener(this);
height = txtViewEx.getHeight();
scrollY = txtViewEx.getScrollY();
Layout layout = txtViewEx.getLayout();
firstVisibleLineNumber = layout.getLineForVertical(scrollY);
lastVisibleLineNumber = layout.getLineForVertical(height+scrollY);
);
【问题讨论】:
【参考方案1】:新答案
PagedTextView 库(在 Kotlin 中)通过扩展 Android TextView 总结了以下谎言算法。 sample app 演示了库的用法。
设置
dependencies
implementation 'com.github.onikx:pagedtextview:0.1.3'
用法
<com.onik.pagedtextview.PagedTextView
android:layout_
android:layout_ />
旧答案
下面的算法在TextView本身的分离中实现了文本分页,缺少TextView属性和算法配置参数的同时动态变化。
背景
我们对TextView
中的文本处理的了解是,它会根据视图的宽度正确地按行分隔文本。查看TextView's sources 我们可以看到文本处理是由Layout 类完成的。所以我们可以利用Layout
类为我们所做的工作,并利用它的方法进行分页。
问题
TextView
的问题在于,文本的可见部分可能会在最后一条可见行的中间某处被垂直剪切。关于上面说的,当遇到完全适合视图高度的最后一行时,我们应该打破一个新页面。
算法
我们遍历文本行并检查行的bottom
是否超过视图的高度;
如果是这样,我们会打开一个新页面并计算累积高度的新值,以将以下行的bottom
与(查看实现)进行比较。新值定义为:top
值(下图中的红线)+TextView's
高度。
实施
public class Pagination
private final boolean mIncludePad;
private final int mWidth;
private final int mHeight;
private final float mSpacingMult;
private final float mSpacingAdd;
private final CharSequence mText;
private final TextPaint mPaint;
private final List<CharSequence> mPages;
public Pagination(CharSequence text, int pageW, int pageH, TextPaint paint, float spacingMult, float spacingAdd, boolean inclidePad)
this.mText = text;
this.mWidth = pageW;
this.mHeight = pageH;
this.mPaint = paint;
this.mSpacingMult = spacingMult;
this.mSpacingAdd = spacingAdd;
this.mIncludePad = inclidePad;
this.mPages = new ArrayList<>();
layout();
private void layout()
final StaticLayout layout = new StaticLayout(mText, mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, mIncludePad);
final int lines = layout.getLineCount();
final CharSequence text = layout.getText();
int startOffset = 0;
int height = mHeight;
for (int i = 0; i < lines; i++)
if (height < layout.getLineBottom(i))
// When the layout height has been exceeded
addPage(text.subSequence(startOffset, layout.getLineStart(i)));
startOffset = layout.getLineStart(i);
height = layout.getLineTop(i) + mHeight;
if (i == lines - 1)
// Put the rest of the text into the last page
addPage(text.subSequence(startOffset, layout.getLineEnd(i)));
return;
private void addPage(CharSequence text)
mPages.add(text);
public int size()
return mPages.size();
public CharSequence get(int index)
return (index >= 0 && index < mPages.size()) ? mPages.get(index) : null;
注 1
该算法不仅适用于TextView
(Pagination
类在上面的实现中使用TextView's
参数)。您可以传递StaticLayout
接受的任何一组参数,然后使用分页布局在Canvas
/Bitmap
/PdfDocument
上绘制文本。
您还可以将Spannable
用作不同字体的yourText
参数以及Html
格式的字符串(如下例所示)。
注2
当所有文本的字体大小相同时,所有行的高度相同。在这种情况下,您可能需要考虑通过计算适合单个页面的行数并在每次循环迭代时跳转到正确的行来进一步优化算法。
示例
下面的示例对包含 html
和 Spanned
文本的字符串进行分页。
public class PaginationActivity extends Activity
private TextView mTextView;
private Pagination mPagination;
private CharSequence mText;
private int mCurrentIndex = 0;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pagination);
mTextView = (TextView) findViewById(R.id.tv);
Spanned htmlString = Html.fromHtml(getString(R.string.html_string));
Spannable spanString = new SpannableString(getString(R.string.long_string));
spanString.setSpan(new ForegroundColorSpan(Color.BLUE), 0, 24, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanString.setSpan(new RelativeSizeSpan(2f), 0, 24, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanString.setSpan(new StyleSpan(Typeface.MONOSPACE.getStyle()), 0, 24, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanString.setSpan(new ForegroundColorSpan(Color.BLUE), 700, spanString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanString.setSpan(new RelativeSizeSpan(2f), 700, spanString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanString.setSpan(new StyleSpan(Typeface.MONOSPACE.getStyle()), 700, spanString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mText = TextUtils.concat(htmlString, spanString);
mTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
@Override
public void onGlobalLayout()
// Removing layout listener to avoid multiple calls
mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
mPagination = new Pagination(mText,
mTextView.getWidth(),
mTextView.getHeight(),
mTextView.getPaint(),
mTextView.getLineSpacingMultiplier(),
mTextView.getLineSpacingExtra(),
mTextView.getIncludeFontPadding());
update();
);
findViewById(R.id.btn_back).setOnClickListener(v ->
mCurrentIndex = (mCurrentIndex > 0) ? mCurrentIndex - 1 : 0;
update();
);
findViewById(R.id.btn_forward).setOnClickListener(v ->
mCurrentIndex = (mCurrentIndex < mPagination.size() - 1) ? mCurrentIndex + 1 : mPagination.size() - 1;
update();
);
private void update()
final CharSequence text = mPagination.get(mCurrentIndex);
if(text != null) mTextView.setText(text);
Activity
的布局:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" >
<LinearLayout
android:layout_
android:layout_>
<Button
android:id="@+id/btn_back"
android:layout_
android:layout_
android:layout_weight="1"
android:background="@android:color/transparent"/>
<Button
android:id="@+id/btn_forward"
android:layout_
android:layout_
android:layout_weight="1"
android:background="@android:color/transparent"/>
</LinearLayout>
<TextView
android:id="@+id/tv"
android:layout_
android:layout_/>
</RelativeLayout>
截图:
【讨论】:
【参考方案2】:看看我的demo project。
“魔法”就在这段代码中:
mTextView.setText(mText);
int height = mTextView.getHeight();
int scrollY = mTextView.getScrollY();
Layout layout = mTextView.getLayout();
int firstVisibleLineNumber = layout.getLineForVertical(scrollY);
int lastVisibleLineNumber = layout.getLineForVertical(height + scrollY);
//check is latest line fully visible
if (mTextView.getHeight() < layout.getLineBottom(lastVisibleLineNumber))
lastVisibleLineNumber--;
int start = pageStartSymbol + mTextView.getLayout().getLineStart(firstVisibleLineNumber);
int end = pageStartSymbol + mTextView.getLayout().getLineEnd(lastVisibleLineNumber);
String displayedText = mText.substring(start, end);
//correct visible text
mTextView.setText(displayedText);
【讨论】:
最后一个单词如果不完整也可以去掉,这样就不会删词了;此外,如果 textview 不可滚动,您可以使用getLineCount()
来获取lastVisibleLineNumber
。这需要使用getViewTreeObserver()
包装在GlobalLayoutListener
中,以便TextView
在计算之前具有高度。【参考方案3】:
令人惊讶的是,要为 Pagination 找到库是很困难的。我认为最好使用除 TextView 之外的另一个 Android UI 元素。 WebView 怎么样? 一个例子@android-webview-example。 代码sn-p:
webView = (WebView) findViewById(R.id.webView1);
String customHtml = "<html><body><h1>Hello, WebView</h1></body></html>";
webView.loadData(customHtml, "text/html", "UTF-8");
注意:这只是将数据加载到 WebView 上,类似于 Web 浏览器。但是,我们不要仅仅停留在这个想法上。通过WebViewClient onPageFinished 将此 UI 添加到使用分页。请阅读 SO 链接 @html-book-like-pagination。 来自 Dan 的最佳答案之一的代码 sn-p:
mWebView.setWebViewClient(new WebViewClient()
public void onPageFinished(WebView view, String url)
...
mWebView.loadUrl("...");
);
注意事项:
代码在页面滚动时加载更多数据。 在同一网页上,Engin Kurutepe 发布了一个答案,用于设置 WebView 的测量值。这是在分页中指定页面所必需的。我还没有实现分页,但我认为这是一个好的开始并且显示出希望,应该很快。可以看到,已经有开发者实现了这个功能。
【讨论】:
以上是关于Android中的分页文本的主要内容,如果未能解决你的问题,请参考以下文章
Android 中 ScrollView 如何实现类似 iPhone 中 UIScrollView 的分页功能?