問題描述
我正在為 Android 編寫一個(gè)簡(jiǎn)單的文本/電子書查看器,所以我使用 TextView
向用戶顯示 HTML 格式的文本,以便他們可以通過返回和瀏覽頁(yè)面中的文本向前.但我的問題是我無法在 Android 中對(duì)文本進(jìn)行分頁(yè).
我不能(或者我不知道如何)從 TextView
用來將文本分成行和頁(yè)的換行和分頁(yè)算法中獲得適當(dāng)?shù)姆答?因此,我無法理解內(nèi)容在實(shí)際顯示中的結(jié)束位置,因此我從下一頁(yè)的剩余部分繼續(xù).我想找到解決這個(gè)問題的方法.
如果我知道屏幕上最后繪制的字符是什么,我可以輕松地放置足夠的字符來填滿屏幕,并且知道實(shí)際繪制完成的位置,我可以繼續(xù)下一頁(yè).這可能嗎?怎么樣?
<小時(shí)>在 StackOverflow 上已多次提出類似問題,但未提供令人滿意的答案.這些只是其中的一部分:
- <小時(shí)>
PS:我不僅限于 TextView,而且我知道換行和分頁(yè)算法可能非常復(fù)雜(
實(shí)施
公共類分頁(yè){私有最終布爾值 mIncludePad;私人最終 int mWidth;私人最終 int mHeight;私人最終浮動(dòng) mSpacingMult;私人最終浮動(dòng) mSpacingAdd;私有最終 CharSequence mText;私有最終 TextPaint mPaint;私有最終列表<CharSequence>mPages;公共分頁(yè)(CharSequence 文本,int pageW,int pageH,TextPaint 油漆,浮動(dòng)間距Mult,浮動(dòng)間距添加,布爾 inclidePad){this.mText = 文本;this.mWidth = pageW;this.mHeight = pageH;this.mPaint = 油漆;this.mSpacingMult = spacingMult;this.mSpacingAdd = 間距添加;this.mIncludePad = inclidePad;this.mPages = new ArrayList<>();布局();}私人無效布局(){final StaticLayout layout = new StaticLayout(mText, mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, mIncludePad);最終 int 行 = layout.getLineCount();final CharSequence text = layout.getText();int startOffset = 0;int 高度 = mHeight;for (int i = 0; i < 行; i++) {如果(高度 < layout.getLineBottom(i)) {//當(dāng)超出布局高度時(shí)addPage(text.subSequence(startOffset, layout.getLineStart(i)));startOffset = layout.getLineStart(i);高度 = layout.getLineTop(i) + mHeight;}如果(我 == 行 - 1){//把剩下的文本放到最后一頁(yè)addPage(text.subSequence(startOffset, layout.getLineEnd(i)));返回;}}}私人無效添加頁(yè)(字符序列文本){mPages.add(文本);}公共整數(shù)大小(){返回 mPages.size();}公共 CharSequence get(int index) {return (index >= 0 && index < mPages.size()) ?mPages.get(index) : null;}}
注 1
該算法不僅適用于
TextView
(Pagination
類在上面的實(shí)現(xiàn)中使用TextView 的
參數(shù)).您可以傳遞StaticLayout
接受的任何參數(shù)集,然后使用分頁(yè)布局在I am writing a simple text/eBook viewer for Android, so I have used a
TextView
to show the HTML formatted text to the users, so they can browse the text in pages by going back and forth. But my problem is that I can not paginate the text in Android.I can not (or I don't know how to) get appropriate feedback from the line-breaking and page-breaking algorithms in which
TextView
uses to break text into lines and pages. Thus, I can not understand where the content ends in the actual display, so that I continue from the remaining in the next page. I want to find way to overcome this problem.If I know what is the last character painted on the screen, I can easily put enough characters to fill a screen, and knowing where tha actual painting was finished, I can continue at the next page. Is this possible? How?
Similar questions have been asked several times on StackOverflow, but no satisfactory answer was provided. These are just a few of them:
- How to paginate long text into pages in Android?
- Ebook reader pagination issue in android
- Paginate text based on rendered text size
There was a single answer, which seems to work, but it is slow. It adds characters and lines until the page is filled. I don't think this is a good way to do page breaking:
- How to break styled text into pages in Android?
Rather than this question, it happens that PageTurner eBook reader does it mostly right, although it is somehow slow.
- https://github.com/nightwhistler/pageturner
PS: I am not confined to TextView, and I know line breaking and page breaking algorithms can be quite complex (as in TeX), so I am not looking for an optimal answer, but rather a reasonably fast solution that can be usable by the users.
Update: This seems to be a good start for getting the right answer:
Is there a way of retrieving a TextView's visible line count or range?
Answer: After completing text layout, it is possible to find out the visible text:
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); } });
解決方案NEW ANSWER
PagedTextView library (in Kotlin) summarises the below lying algorithm by extending Android TextView. The sample app demonstrates the usage of the library.
Setup
dependencies { implementation 'com.github.onikx:pagedtextview:0.1.3' }
Usage
<com.onik.pagedtextview.PagedTextView android:layout_width="match_parent" android:layout_height="match_parent" />
OLD ANSWER
The algorithm below implements text pagination in separation of TextView itself lacking simultaneous dynamic change of both the TextView attributes and algorithm configuration parameters.
Background
What we know about text processing within
TextView
is that it properly breaks a text by lines according to the width of a view. Looking at the TextView's sources we can see that the text processing is done by the Layout class. So we can make use of the work theLayout
class does for us and utilizing its methods do pagination.Problem
The problem with
TextView
is that the visible part of text might be cut vertically somewhere at the middle of the last visible line. Regarding said, we should break a new page when the last line that fully fits into a view's height is met.Algorithm
- We iterate through the lines of text and check if the line's
bottom
exceeds the view's height; - If so, we break a new page and calculate a new value for the cumulative height to compare the following lines'
bottom
with (see the implementation). The new value is defined astop
value (red line in the picture below) of the line that hasn't fit into the previous page +TextView's
height.
Implementation
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; } }
Note 1
The algorithm works not just for
TextView
(Pagination
class usesTextView's
parameters in the implementation above). You may pass any set of parametersStaticLayout
accepts and later use the paginated layouts to draw text onCanvas
/Bitmap
/PdfDocument
.You can also use
Spannable
asyourText
parameter for different fonts as well asHtml
-formatted strings (like in the sample below).Note 2
When all text has the same font size, all lines have equal height. In that case you might want to consider further optimization of the algorithm by calculating an amount of lines that fits into a single page and jumping to the proper line at each loop iteration.
Sample
The sample below paginates a string containing both
html
andSpanned
text.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
's layout:<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>
Screenshot:
這篇關(guān)于Android中的分頁(yè)文本的文章就介紹到這了,希望我們推薦的答案對(duì)大家有所幫助,也希望大家多多支持html5模板網(wǎng)!
【網(wǎng)站聲明】本站部分內(nèi)容來源于互聯(lián)網(wǎng),旨在幫助大家更快的解決問題,如果有圖片或者內(nèi)容侵犯了您的權(quán)益,請(qǐng)聯(lián)系我們刪除處理,感謝您的支持!