Android中的分页文本

时间:2022-11-12
本文介绍了Android中的分页文本的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

我正在为 Android 编写一个简单的文本/电子书查看器,所以我使用 TextView 向用户显示 HTML 格式的文本,以便他们可以通过返回和浏览页面中的文本向前.但我的问题是我无法在 Android 中对文本进行分页.

我不能(或者我不知道如何)从 TextView 用来将文本分成行和页的换行和分页算法中获得适当的反馈.因此,我无法理解内容在实际显示中的结束位置,因此我从下一页的剩余部分继续.我想找到解决这个问题的方法.

如果我知道屏幕上最后绘制的字符是什么,我可以轻松地放置足够的字符来填满屏幕,并且知道实际绘制完成的位置,我可以继续下一页.这可能吗?怎么样?

<小时>

在 StackOverflow 上已多次提出类似问题,但未提供令人满意的答案.这些只是其中的一部分:

  • <小时>

    PS:我不仅限于 TextView,而且我知道换行和分页算法可能非常复杂(

    实施

    公共类分页{私有最终布尔值 mIncludePad;私人最终 int mWidth;私人最终 int mHeight;私人最终浮动 mSpacingMult;私人最终浮动 mSpacingAdd;私有最终 CharSequence mText;私有最终 TextPaint mPaint;私有最终列表<CharSequence>mPages;公共分页(CharSequence 文本,int pageW,int pageH,TextPaint 油漆,浮动间距Mult,浮动间距添加,布尔 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)) {//当超出布局高度时addPage(text.subSequence(startOffset, layout.getLineStart(i)));startOffset = layout.getLineStart(i);高度 = layout.getLineTop(i) + mHeight;}如果(我 == 行 - 1){//把剩下的文本放到最后一页addPage(text.subSequence(startOffset, layout.getLineEnd(i)));返回;}}}私人无效添加页(字符序列文本){mPages.add(文本);}公共整数大小(){返回 mPages.size();}公共 CharSequence get(int index) {return (index >= 0 && index < mPages.size()) ?mPages.get(index) : null;}}

    注 1

    该算法不仅适用于 TextView(Pagination 类在上面的实现中使用 TextView 的 参数).您可以传递 StaticLayout 接受的任何参数集,然后使用分页布局在

    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 the Layout 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 as top 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 uses TextView's parameters in the implementation above). You may pass any set of parameters StaticLayout accepts and later use the paginated layouts to draw text on Canvas/Bitmap/PdfDocument.

    You can also use Spannable as yourText parameter for different fonts as well as Html-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 and Spanned 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:

    这篇关于Android中的分页文本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!

上一篇:android on Text Change Listener 下一篇:指定的孩子已经有一个父母.您必须首先在孩子的父母上调用 removeView() (Android)

相关文章

最新文章