新答案
PagedTextView库(使用Kotlin编写)通过扩展Android TextView,概述了下面的算法。 示例应用程序演示了该库的用法。
设置
dependencies {
implementation 'com.github.onikx:pagedtextview:0.1.3'
}
使用方法
<com.onik.pagedtextview.PagedTextView
android:layout_width="match_parent"
android:layout_height="match_parent" />
旧答案
以下算法实现了文本分页,将TextView本身的属性和算法配置参数同时动态更改。
背景
我们知道在TextView
中处理文本时,它会根据视图的宽度正确地将文本分行。查看TextView源代码,我们可以看到文本处理是由Layout类完成的。因此,我们可以利用Layout
类为我们完成的工作,并利用其方法进行分页。
问题
TextView
的问题在于,可见部分的文本可能会在最后一行的中间垂直切断。因此,当满足最后一行完全适合视图高度时,我们应该分隔一个新页面。
算法
- 我们遍历文本的每一行,并检查行的底部是否超过视图的高度。
- 如果是,则我们分隔一个新页面,并计算一个新值作为累积高度,以与以下行的
bottom
进行比较(请参见实现)。新值被定义为未适合前一页的行的top
值(下图中的红线)+ TextView
的高度。
实现
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)) {
addPage(text.subSequence(startOffset, layout.getLineStart(i)));
startOffset = layout.getLineStart(i);
height = layout.getLineTop(i) + mHeight;
}
if (i == lines - 1) {
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
该算法不仅适用于 TextView
(Pagination
类在上面的实现中使用了 TextView
的参数)。您可以传递任何 StaticLayout
接受的一组参数,并稍后使用分页布局在Canvas
/Bitmap
/PdfDocument
上绘制文本。
您还可以将 Spannable
作为 yourText
参数使用,以获取不同字体以及 Html
格式化的字符串(就像下面的示例一样)。
注2
当所有文本具有相同的字体大小时,所有行的高度都相等。在这种情况下,您可能希望通过计算适合单个页面的行数并在每个循环迭代中跳转到适当的行来进一步优化算法。
示例
下面的示例对包含 html
和 Spanned
文本的字符串进行分页。
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() {
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
的布局:
<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>
截图:
getLineCount()
来获取lastVisibleLineNumber
。这需要使用getViewTreeObserver()
将其包装在GlobalLayoutListener
中,以便在计算之前TextView
具有高度。 - Andrei Tudor Diaconu