Подтвердить что ты не робот

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

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

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

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


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

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

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

снимок экрана с помощью устройства чтения PageTurner eBook


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

            }
        });
4b9b3361

Ответ 1

Фон

Что мы знаем о обработке текста в TextView, так это то, что он правильно разбивает текст по строкам в соответствии с шириной представления. Глядя на TextView's sources, мы видим, что обработка текста выполняется с помощью Layout. Таким образом, мы можем использовать работу, которую класс Layout делает для нас, и использование его методов делает разбиение на страницы.

Проблема

Проблема с TextView заключается в том, что видимая часть текста может быть разрезана вертикально где-то посередине последней видимой строки. Что касается сказанного, мы должны сломать новую страницу, когда будет достигнута последняя строка, полностью вписывающаяся в высоту представления.

Алгоритм

  • Мы повторяем строки текста и проверяем, превышает ли строка bottom высоту представления;
  • Если это так, мы разбиваем новую страницу и вычисляем новое значение для кумулятивной высоты для сравнения следующих строк: bottom с (см. реализацию). Новое значение определяется как top (красная линия на рисунке ниже) строки, которая не вписывается в предыдущую страницу + 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;
    }
}

Примечание 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>

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

Ответ 2

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

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

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

Ответ 3

Удивительно найти библиотеки для Pagination сложно. Я считаю, что лучше использовать другой элемент интерфейса Android помимо TextView. Как насчет WebView? Пример @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("...");
   }
});

Примечания:

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

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