Разметка текста на Android

Я пишу простой текстовый / eBook-просмотрщик для Android, поэтому я использовал TextView для отображения текста в формате HTML для пользователей, чтобы они могли просматривать текст на страницах, переходя туда и обратно. Но моя проблема в том, что я не могу разбивать текст на Android.

Я не могу (или не знаю, как) получить соответствующую обратную связь от алгоритмов разрыва строки и разбиения на страницы, в которых TextView использует для разбиения текста на строки и страницы. Таким образом, я не могу понять, где содержимое заканчивается на фактическом дисплее, поэтому я продолжаю с оставшегося на следующей странице. Я хочу найти способ преодолеть эту проблему.

Если я знаю, что является последним персонажем, нарисованным на экране, я могу легко разместить достаточно символов, чтобы заполнить экран, и зная, где закончилась фактическая живопись, я могу продолжить на следующей странице. Это возможно? Как?


Аналогичные вопросы задавались несколько раз в StackOverflow, но удовлетворительный ответ не был предоставлен. Это лишь некоторые из них:

  • Как разбивать длинный текст на страницы в Android?
  • Проблема с разбивкой по Ebook-читателю в android
  • Текст с разбивкой по размеру текста

Был один ответ, который, похоже, работает, но он медленный. Он добавляет символы и строки до тех пор, пока страница не будет заполнена. Я не думаю, что это хороший способ сделать разрыв страницы:

  • Как разбить стилизованный текст на страницы в Android?

Вместо этого возникает вопрос, что читатель электронных книг PageTurner делает это в основном правильно, хотя он как-то медленный.

  • https://github.com/nightwhistler/pageturner

скриншот программы чтения 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); } }); 

Задний план

То, что мы знаем о обработке текста в TextView состоит в том, что он правильно разбивает текст по строкам в соответствии с шириной представления. Посмотрев на источники TextView's мы видим, что обработка текста выполняется classом Layout . Таким образом, мы можем использовать работу, которую class 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 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; } } 

Примечание 1

Алгоритм работает не только для TextView (class TextView's использует параметры TextView's в реализации выше). Вы можете передать любой набор параметров, которые StaticLayout принимает, а затем использовать StaticLayout на страницы макеты для рисования текста в Canvas / Bitmap / PdfDocument .

Вы также можете использовать Spannable в yourText параметра yourText для разных шрифтов, а также для строковых строк ( как в примере ниже ).

Заметка 2

Когда весь текст имеет одинаковый размер шрифта, все линии имеют равную высоту. В этом случае вам может потребоваться дальнейшая оптимизация алгоритма, вычисляя количество строк, которые вписываются в одну страницу, и прыгают в соответствующую строку на каждой итерации цикла.


Образец

В приведенном ниже примере выполняется разбиение на страницы строки, содержащей как html и 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); } } 

Макет мероприятия:

        

Скриншот: введите описание изображения здесь

Взгляните на мой демонстрационный проект .

«Магия» находится в этом коде:

  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 ? Пример примера @ android-webview . Фрагмент кода:

 webView = (WebView) findViewById(R.id.webView1); String customHtml = "

Hello, WebView

"; webView.loadData(customHtml, "text/html", "UTF-8");

Примечание. Это просто загружает данные в WebView, подобно веб-браузеру. Но давайте не будем останавливаться только на этой идее. Добавьте этот интерфейс для использования разбиения на страницы с помощью WebViewClient onPageFinished . Пожалуйста, прочитайте SO link @ html-book-like-pagination . Фрагмент кода из одного из лучших ответов Дэна:

 mWebView.setWebViewClient(new WebViewClient() { public void onPageFinished(WebView view, String url) { ... mWebView.loadUrl("..."); } }); 

Заметки:

  • Код загружает больше данных при прокрутке страницы.
  • На той же веб-странице есть ответ от Engin Kurutepe, чтобы установить измерения для WebView. Это необходимо для указания страницы в разбивке на страницы.

Я не реализовал разбивку на страницы, но я думаю, что это хорошее начало и показывает promise, должно быть быстро. Как вы можете видеть, есть разработчики, которые внедрили эту функцию.

  • JRE 1.7 - версия java - возвращает: java / lang / NoClassDefFoundError: java / lang / Object
  • NoClassDefFoundError - Eclipse и Android
  • BitmapFactory.decodeResource возвращает измененный битмап в Android 2.2 и неизменный Bitmap в Android 1.6
  • Theme.AppCompat.Light.DarkActionBar - Ресурс не найден
  • Countdowntimer в минутах и ​​секундах
  • Как распечатать изображение на Bluetooth-принтере в Android?
  • Android java.lang.NoClassDefFoundError: org.jsoup.Jsoup
  • Использование неинициализированного конечного поля - с / без "этого". спецификатор
  • Как найти программные приложения, запущенные в настоящее время, в Android?
  • Как записать файлы на внешнее общедоступное хранилище на Android, чтобы они были видны из Windows?
  • Как установить выбранный элемент Spinner по значению, а не положением?
  • Давайте будем гением компьютера.