我写一个简单的文本/电子书阅读器为Android,所以我使用了一个的TextView
显示HTML格式的文本给用户,这样他们就可以浏览文本在通过来回页面。但我的问题是,我不能在分页Android的文本。
我不能(或我不知道如何),您可以通过断行和页打破算法,其中的TextView
适当的反馈用来打破文本行和页。因此,我不明白,在实际显示其中内容结束后,让我继续从剩余的下一个页面。我想找到方法来克服这个问题。
如果我知道了什么是画在屏幕上的最后一个字符,我可以很容易地把足够的字符来填充屏幕,并且知道在哪里THA实际画完成后,我可以继续下一个页面。这可能吗?怎么样?
类似的问题已经被问过的StackOverflow了好几次,但没有提供令人满意的答案。这些只是其中的几个:
How进行分页长文成Android的网页? 在Android的 电子书阅读器的分页问题 Paginate基于呈现的文本大小的文本有一个单一的答案,这似乎是工作,但它是缓慢的。它增加了字符和线条直到页面被充满。我不认为这是一个好办法页断:
How打破样式文本到网页中的Android?而不是这个问题,它发生的 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);
}
});
解决方案
您说得对,的 ...添加字符和线条,直到页面充满
的是不是一个好办法做到分页。
我们知道在的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,mWidth,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(指数):空;
}
}
该算法不仅对的TextView
(分页
类使用 TextView中的
在执行以上参数)。你可以通过任何一组参数 StaticLayout
接受,后来使用分页布局,在画布
/ 位图 / PdfDocument
。
您也可以使用 Spannable
为 yourText
参数不同的字体,以及 HTML
格式化的字符串(如下面的示例中的)。
在所有文本具有相同的字体大小,所有行高度相等。在这种情况下,您可能希望通过计算适合到一个单一的网页线量和跳跃到正确的路线在每次循环迭代考虑算法的进一步优化。
=============================================== =========================
样品下面进行分页包含字符串既 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:机器人=http://schemas.android.com/apk/res/android
机器人:layout_width =match_parent
机器人:layout_height =match_parent
机器人:以下属性来=@扪/ activity_horizontal_margin
机器人:paddingRight =@扪/ activity_horizontal_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 sizeThere 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/pageturnerPS: 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.
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.
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.
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.
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;
}
}
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).
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.
========================================================================
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: