Разбиение текста на страницы в Android
Я пишу простой просмотрщик текста / электронных книг для Android, поэтому я использовал TextView
показывать текст в формате HTML пользователям, чтобы они могли просматривать текст на страницах, перемещаясь назад и вперед. Но моя проблема в том, что я не могу разбить текст на страницы в Android.
Я не могу (или не знаю, как) получить соответствующую обратную связь от алгоритмов разрыва строк и разрыва страниц, в которых TextView
используется для разбивки текста на строки и страницы. Таким образом, я не могу понять, где заканчивается содержимое на реальном дисплее, так что я продолжаю с оставшегося на следующей странице. Я хочу найти способ преодолеть эту проблему.
Если я знаю, какой последний символ нарисован на экране, я могу легко поместить достаточно символов, чтобы заполнить экран, и, зная, где была закончена настоящая живопись, я могу продолжить на следующей странице. Это возможно? Как?
Подобные вопросы задавались несколько раз в Stackru, но удовлетворительного ответа не было. Вот лишь некоторые из них:
- Как разбить длинный текст на страницы в Android?
- Ebook читатель вопрос о нумерации страниц в Android
- Разбивка текста на основе размера отрисованного текста
Был один ответ, который, кажется, работает, но он медленный. Он добавляет символы и строки, пока страница не будет заполнена. Я не думаю, что это хороший способ разбить страницу:
Вместо того, чтобы ответить на этот вопрос, случается, что программа чтения электронных книг PageTurner делает это в основном правильно, хотя и как-то медленно.
PS: я не ограничен TextView, и я знаю, что алгоритмы разрыва строк и разрыва страниц могут быть довольно сложными ( как в TeX), поэтому я не ищу оптимальный ответ, а скорее достаточно быстрое решение, которое может быть использовано пользователи.
Обновление: это, кажется, хорошее начало для получения правильного ответа:
Есть ли способ получить видимый счетчик строк или диапазон TextView?
Ответ: После завершения разметки текста можно узнать видимый текст:
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);
}
});
3 ответа
Фон
Что мы знаем об обработке текста в TextView
в том, что он правильно разбивает текст по строкам в соответствии с шириной представления. Глядя на TextView's
Источники мы можем видеть, что обработка текста выполняется Layout
учебный класс. Таким образом, мы можем использовать работу Layout
класс делает для нас и используя свои методы, делает нумерацию страниц.
проблема
Проблема с TextView
в том, что видимая часть текста может быть вырезана вертикально где-то посередине последней видимой строки. Что касается сказанного, мы должны разбить новую страницу, когда будет достигнута последняя строка, которая полностью соответствует высоте представления.
Алгоритм
- Мы перебираем строки текста и проверяем,
bottom
превышает высоту вида; - Если это так, мы разбиваем новую страницу и вычисляем новое значение для совокупной высоты, чтобы сравнить следующие строки:
bottom
с (см. реализацию). Новое значение определяется какtop
значение (красная линия на рисунке ниже) строки, которая не вписывается в предыдущую страницу +TextView's
рост.
Реализация
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;
}
}
Примечание 1
Алгоритм работает не только для TextView
(Pagination
класс использует TextView's
параметры при реализации выше). Вы можете передать любой набор параметров 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() {
// 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
макет:
<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>
Посмотрите на мой демонстрационный проект.
"Магия" в этом коде:
mTextView.setText(mText);
int height = mTextView.getHeight();
int scrollY = mTextView.getScrollY();
Layout layout = mTextView.getLayout();
int firstVisibleLineNumber = layout.getLineForVertical(scrollY);
int lastVisibleLineNumber = layout.getLineForVertical(height + scrollY);
//check is latest line fully visible
if (mTextView.getHeight() < layout.getLineBottom(lastVisibleLineNumber)) {
lastVisibleLineNumber--;
}
int start = pageStartSymbol + mTextView.getLayout().getLineStart(firstVisibleLineNumber);
int end = pageStartSymbol + mTextView.getLayout().getLineEnd(lastVisibleLineNumber);
String displayedText = mText.substring(start, end);
//correct visible text
mTextView.setText(displayedText);
Удивительно, но найти библиотеки для нумерации страниц сложно. Я думаю, что лучше использовать другой элемент пользовательского интерфейса Android, кроме TextView. Как насчет WebView? Пример @ http://www.mkyong.com/android/android-webview-example/. Фрагмент кода:
webView = (WebView) findViewById(R.id.webView1);
String customHtml = "<html><body><h1>Hello, WebView</h1></body></html>";
webView.loadData(customHtml, "text/html", "UTF-8");
Примечание. Это просто загружает данные в WebView, аналогично веб-браузеру. Но давайте не будем останавливаться только на этой идее. Добавьте этот пользовательский интерфейс для использования нумерации страниц через WebViewClient onPageFinished. Пожалуйста, прочитайте по ссылке SO @ html-book-like-pagination. Фрагмент кода от одного из лучших ответов Дэна:
mWebView.setWebViewClient(new WebViewClient() {
public void onPageFinished(WebView view, String url) {
...
mWebView.loadUrl("...");
}
});
Заметки:
- Код загружает больше данных при прокрутке страницы.
- На той же веб-странице размещен ответ Энгина Курутепе, чтобы установить измерения для WebView. Это необходимо для указания страницы в нумерации страниц.
Я не реализовал нумерацию страниц, но думаю, что это хорошее начало и обещает быть быстрым. Как видите, есть разработчики, которые реализовали эту функцию.