在Android的分页文本分页、文本、Android

2023-09-12 09:13:58 作者:包裹自己送出地球。

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

我不能(或我不知道如何),您可以通过断行和页打破算法,其中的TextView 适当的反馈用来打破文本行和页。因此,我不明白,在实际显示其中内容结束后,让我继续从剩余的下一个页面。我想找到方法来克服这个问题。

如果我知道了什么​​是画在屏幕上的最后一个字符,我可以很容易地把足够的字符来填充屏幕,并且知道在哪里THA实际画完成后,我可以继续下一个页面。这可能吗?怎么样?

类似的问题已经被问过的StackOverflow了好几次,但没有提供令人满意的答案。这些只是其中的几个:

How进行分页长文成Android的网页? 在Android的 电子书阅读器的分页问题 Paginate基于呈现的文本大小的文本

有一个单一的答案,这似乎是工作,但它是缓慢的。它增加了字符和线条直到页面被充满。我不认为这是一个好办法页断:

How打破样式文本到网页中的Andr​​oid?

而不是这个问题,它发生的 PageTurner电子书阅读器 做它基本上​​是正确的,但它是有点慢。

https://github.com/nightwhistler/pageturner

PS:我不是局限于TextView的,我知道断行和页面断裂算法可以很复杂(如TeX的),所以我不是在寻找一个最佳的答案,而是一个相当快的解决方案,可以由用户使用。

更新:这似乎是一个良好的开端得到正确的答案:

Is有检索一个TextView的可视行数或范围的一种方式?

答:完成文本布局后,就可以找出可见文本:

  ViewTreeObserver VTO = txtViewEx.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(新OnGlobalLayoutListener(){
            @覆盖
            公共无效onGlobalLayout(){
                ViewTreeObserver观测值= txtViewEx.getViewTreeObserver();
                obs.removeOnGlobalLayoutListener(本);
                身高= txtViewEx.getHeight();
                scrollY = txtViewEx.getScrollY();
                布局布局= txtViewEx.getLayout();

                firstVisibleLineNumber = layout.getLineForVertical(scrollY);
                lastVisibleLineNumber = layout.getLineForVertical(高度+ scrollY);

            }
        });
 
Android包含多个颜色的背景,关于Android Android 如何设置所有屏幕的背景色

解决方案

您说得对,的 ...添加字符和线条,直到页面充满 的是不是一个好办法做到分页。

背景

我们知道在的TextView 文本处理是什么,它​​正确地根据视图的宽度打破了由文本行。纵观 TextView中的来源中,我们可以看到,文本处理被完成的 布局 类。因此,我们可以使用布局类为我们做的工作,并利用它的方法做分页。

问题

的TextView 的问题是,文字的可见部分可能会被垂直的地方在最后一个可见行的中间切开。至于说,我们要打破的时候,充分融入一个视图的高度最后一行遇到了新的一页。

算法

我们遍历文本行,并检查行的超过视图的高度; 如果是这样,我们打破了新的一页,并计算出新的价值,为累积高度比较以下行用(看到实施)。新的值被定义为值(红线以下的图片)还没有融入previous行页面+ TextView中的的高度。

实施

 公共类分页{
    私人最终布尔mIncludePad;
    私人最终诠释mWidth;
    私人最终诠释mHeight;
    私人最终浮动mSpacingMult;
    私人最终浮动mSpacingAdd;
    私人最终CharSequence的多行文字;
    私人最终TextPaint mPaint;
    私人最终名单,其中,为CharSequence> mPages;

公共分页(CharSequence的文字,诠释pageW,INT pageH,TextPaint油漆,浮spacingMult,浮spacingAdd,布尔inclidePad){
    this.mText =文本;
    this.mWidth = pageW;
    this.mHeight = pageH;
    this.mPaint =漆;
    this.mSpacingMult = spacingMult;
    this.mSpacingAdd = spacingAdd;
    this.mIncludePad = inclidePad;
    this.mPages =新的ArrayList< CharSequence的>();

    布局();
}

私人无效布局(){
    最后StaticLayout布局=新StaticLayout(MTEXT,mPaint,m​​Width,Layout.Alignment.ALIGN_NORMAL,mSpacingMult,mSpacingAdd,mIncludePad);

    最终诠释行= layout.getLineCount();
    最后的CharSequence文本= layout.getText();
    INT startOffset = 0;
    INT高= mHeight;

    的for(int i = 0; I<线;我++){
        如果(高度&其中; layout.getLineBottom(ⅰ)){
            //当布局高度已超过
            addPage(text.subSequence(startOffset,layout.getLineStart(I)));
            startOffset = layout.getLineStart(ⅰ);
            高度= layout.getLineTop(ⅰ)+ mHeight;
        }

        如果(我==线 -  1){
            //把文本的其余部分到最后一页
            addPage(text.subSequence(startOffset,layout.getLineEnd(I)));
            返回;
        }
    }
}

私人无效addPage(CharSequence的文字){
    mPages.add(文本);
}

公众诠释大小(){
    返回mPages.size();
}

公共CharSequence的GET(INT指数){
    返程(索引> = 0&功放;&安培;指数< mPages.size())? mPages.get(指数):空;
}
}
 

注1

该算法不仅对的TextView 分页类使用 TextView中的在执行以上参数)。你可以通过任何一组参数 StaticLayout 接受,后来使用分页布局,在画布 / 位图 / PdfDocument

您也可以使用 Spannable yourText 参数不同的字体,以及 HTML 格式化的字符串(如下面的示例中的)。

注2

在所有文本具有相同的字体大小,所有行高度相等。在这种情况下,您可能希望通过计算适合到一个单一的网页线量和跳跃到正确的路线在每次循环迭代考虑算法的进一步优化。

=============================================== =========================

示例

样品下面进行分页包含字符串既 HTML 跨区文本。

 公共类PaginationActivity延伸活动{
私人TextView的mTextView;
私人分页mPagination;
私人CharSequence的多行文字;
私人诠释mCurrentIndex = 0;

@覆盖
保护无效的onCreate(包savedInstanceState){
    super.onCreate(savedInstanceState);
    的setContentView(R.layout.activity_pagination);

    mTextView =(TextView中)findViewById(R.id.tv);

    跨区htmlString = Html.fromHtml(的getString(R.string.html_string));

    Spannable spanString =新SpannableString(的getString(R.string.long_string));
    spanString.setSpan(新ForegroundColorSpan(Color.BLUE),0,24,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    spanString.setSpan(新RelativeSizeSpan(2F),0,24,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    spanString.setSpan(新StyleSpan(Typeface.MONOSPACE.getStyle()),0,24,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    spanString.setSpan(新ForegroundColorSpan(Color.BLUE),700,spanString.length(),Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    spanString.setSpan(新RelativeSizeSpan(2F),700,spanString.length(),Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    spanString.setSpan(新StyleSpan(Typeface.MONOSPACE.getStyle()),700,spanString.length(),Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

    多行文字= TextUtils.concat(htmlString,spanString);

    mTextView.getViewTreeObserver()。addOnGlobalLayoutListener(新ViewTreeObserver.OnGlobalLayoutListener(){
        @覆盖
        公共无效onGlobalLayout(){
            //删除布局监听器,以避免多次调用
            如果(Build.VERSION.SDK_INT< Build.VERSION_ codeS.JELLY_BEAN){
                。mTextView.getViewTreeObserver()removeGlobalOnLayoutListener(本);
            } 其他 {
                。mTextView.getViewTreeObserver()removeOnGlobalLayoutListener(本);
            }
            mPagination =新分页(MTEXT,
                    mTextView.getWidth(),
                    mTextView.getHeight(),
                    mTextView.getPaint(),
                    mTextView.getLineSpacingMultiplier(),
                    mTextView.getLineSpacingExtra(),
                    mTextView.getIncludeFontPadding());
            更新();
        }
    });

    findViewById(R.id.btn_back).setOnClickListener(新View.OnClickListener(){
        @覆盖
        公共无效的onClick(视图v){
            mCurrentIndex =(mCurrentIndex大于0)? mCurrentIndex  -  1:0;
            更新();
        }
    });
    findViewById(R.id.btn_forward).setOnClickListener(新View.OnClickListener(){
        @覆盖
        公共无效的onClick(视图v){
            mCurrentIndex =(mCurrentIndex&其中; mPagination.size() -  1)? mCurrentIndex + 1:mPagination.size() -  1;
            更新();
        }
    });
}

私人无效更新(){
    最后的CharSequence文本= mPagination.get(mCurrentIndex);
    如果(文字!= NULL)mTextView.setText(文本);
}
}
 

活动的布局:

 < RelativeLayout的的xmlns:机器人=htt​​p://schemas.android.com/apk/res/android
机器人:layout_width =match_parent
机器人:layout_height =match_parent
机器人:以下属性来=@扪/ activity_horizo​​ntal_margin
机器人:paddingRight =@扪/ activity_horizo​​ntal_margin
机器人:paddingTop =@扪/ activity_vertical_margin
机器人:paddingBottom会=@扪/ activity_vertical_margin>

<的LinearLayout
    机器人:layout_width =match_parent
    机器人:layout_height =match_parent>

    <按钮
        机器人:ID =@ + ID / btn_back
        机器人:layout_width =0dp
        机器人:layout_height =match_parent
        机器人:layout_weight =1
        机器人:背景=@机器人:彩色/透明/>

    <按钮
        机器人:ID =@ + ID / btn_forward
        机器人:layout_width =0dp
        机器人:layout_height =match_parent
        机器人:layout_weight =1
        机器人:背景=@机器人:彩色/透明/>

< / LinearLayout中>

<的TextView
    机器人:ID =@ + ID /电视
    机器人:layout_width =match_parent
    机器人:layout_height =match_parent/>

< / RelativeLayout的>
 

截图:

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

            }
        });

解决方案

You're right that "...adding characters and lines until the page is filled" isn't a good way to do pagination.

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

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
            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'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: