在Android中分页文字

我正在为Android编写一个简单的文本/电子书阅读器,所以我使用了TextView来向用户显示HTML格式的文本,所以他们可以通过来回浏览页面中的文本。 但我的问题是,我不能在Android中分页。

我不能(或者我不知道如何)从断行和分页algorithm中得到适当的反馈,在这些algorithm中, TextView用来将文本分解成行和页面。 因此,我无法理解内容在实际显示中的结束位置,以便我从下一页的剩余内容继续。 我想find办法来克服这个问题。

如果我知道屏幕上画的最后一个字是什么,我可以很容易地把足够的字符填满一个屏幕,并且知道实际的绘画完成的地方,我可以继续下一页。 这可能吗? 怎么样?


类似的问题已经在StackOverflow上多次提出,但没有提供令人满意的答案。 这些只是其中的几个:

  • 如何在Android中将长文本分页成页面?
  • 在Android的电子书阅读器分页问题
  • 根据呈现的文本大小分页文本

有一个答案,似乎工作,但速度缓慢。 它添加字符和行,直到页面被填充。 我不认为这是一个很好的方法来做页面打破:

  • 如何在Android中将样式文本分解成页面?

而不是这个问题,恰巧PageTurner电子书阅读器做它大部分是正确的,虽然它在某种程度上是缓慢的。

  • https://github.com/nightwhistler/pageturner

PageTurner电子书阅读器的屏幕截图


PS:我不仅限于TextView,而且我知道换行和分页algorithm可能相当复杂( 如在TeX中 ),所以我不是在寻找最佳答案,而是一个相当快速的解决scheme,可以被用户。


更新:这似乎是得到正确答案的好开始:

有没有办法检索TextView的可见行数或范围?

答案:完成文字布局后,可以找出可见的文字:

 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); } }); 

背景

我们对TextView文本处理知道的是,它根据视图的宽度按行正确地分割文本。 看看TextView's源代码,我们可以看到文本处理由Layout类完成。 所以我们可以利用Layout类为我们做的工作,并利用它的方法做分页。

问题

TextView的问题是文本的可见部分可能会在最后一个可见行的中间垂直切割。 关于这一点,我们应该在完全符合视图高度的最后一行符合时打破新的一页。

algorithm

  • 我们遍历文本行并检查行的bottom超过视图的高度;
  • 如果是这样,我们打破一个新的页面,并计算累计高度的新值,以比较下面几行的bottom见实现 )。 新值被定义为不符合上一页+ TextView's高度的行的top值(下图中的红线 )。

在这里输入图像描述

履行

 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<CharSequence>(); 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

该algorithm不仅适用于TextViewPagination类在上面的实现中使用TextView's参数)。 您可以传递StaticLayout接受的任何一组参数,然后使用分页布局在Canvas / Bitmap / PdfDocument上绘制文本。

您也可以使用Spannable作为yourText参数,以及不同的字体以及Html格式的string( 如下面的示例中所示 )。

笔记2

当所有文本具有相同的字体大小时,所有行都具有相同的高度。 在这种情况下,您可能需要考虑进一步优化algorithm,方法是计算适合单个页面的行数,并在每次循环迭代时跳至适当的行。


样品

下面的示例分页包含htmlSpanned文本的string。

 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 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { mTextView.getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { 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(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex > 0) ? mCurrentIndex - 1 : 0; update(); } }); findViewById(R.id.btn_forward).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View 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_width="match_parent" android:layout_height="match_parent" 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_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/btn_back" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="@android:color/transparent"/> <Button android:id="@+id/btn_forward" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="@android:color/transparent"/> </LinearLayout> <TextView android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout> 

截图: 在这里输入图像描述

看看我的演示项目 。

这个“魔术”就是在这个代码中:

  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之外,还可以使用另一个Android UI元素。 WebView怎么样? 示例@ android-webview-example 。 代码片段:

 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浏览器。 但是,让我们不要停止这个想法。 将此UI添加到WebViewClient onPageFinished的使用分页。 请阅读SO链接@ html-book-like-pagination 。 Dan的最佳答案之一的代码片段:

 mWebView.setWebViewClient(new WebViewClient() { public void onPageFinished(WebView view, String url) { ... mWebView.loadUrl("..."); } }); 

笔记:

  • 代码在页面滚动时加载更多的数据。
  • 在同一个网页上,Engin Kurutepe发布了一个答案来设置WebView的度量。 这对于在分页中指定页面是必需的。

我没有实施分页,但我认为这是一个好的开始,并显示承诺,应该是快。 正如你所看到的,有开发者已经实现了这个function。