Pregunta Paginating texto en Android


Estoy escribiendo un texto simple / visor de libros electrónicos para Android, así que he usado un TextView para mostrar el texto con formato HTML a los usuarios, para que puedan navegar por el texto en las páginas yendo y viniendo. Pero mi problema es que no puedo paginar el texto en Android.

No puedo (o no sé cómo) obtener los comentarios adecuados de los algoritmos de salto de línea y de salto de página en los que TextView utiliza para dividir el texto en líneas y páginas. Por lo tanto, no puedo entender dónde termina el contenido en la pantalla actual, para que continúe desde el resto en la página siguiente. Quiero encontrar la forma de superar este problema.

Si sé cuál es el último personaje pintado en la pantalla, puedo poner fácilmente suficientes caracteres para llenar una pantalla, y sabiendo dónde terminó la pintura, puedo continuar en la siguiente página. es posible? ¿Cómo?


Se han formulado preguntas similares varias veces en StackOverflow, pero no se proporcionó una respuesta satisfactoria. Estos son sólo algunos de ellos:

Hubo una respuesta única, que parece funcionar, pero es lenta. Agrega caracteres y líneas hasta que se llena la página. No creo que esta sea una buena forma de romper páginas:

En lugar de esta pregunta, sucede que Lector de libros electrónicos de PageTurner lo hace principalmente bien, aunque de alguna manera es lento.

screenshot of PageTurner eBook reader


PD: No estoy limitado a TextView, y sé que los algoritmos de salto de línea y salto de página pueden ser bastante complejos (como en TeX), por lo que no estoy buscando una respuesta óptima, sino más bien una solución razonablemente rápida que pueda ser utilizada por los usuarios.


Actualizar: Este parece ser un buen comienzo para obtener la respuesta correcta:

¿Hay alguna forma de recuperar el conteo o rango de líneas visibles de TextView?

Responder: Después de completar el diseño del texto, es posible encontrar el texto visible:

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

            }
        });

32
2017-08-05 16:19


origen


Respuestas:


Fondo

Lo que sabemos sobre el procesamiento de texto dentro de TextView es que rompe correctamente un texto por líneas de acuerdo con el ancho de una vista. Mirando a la TextView's fuentes podemos ver que el procesamiento de texto se realiza por el Layout clase. Entonces podemos hacer uso del trabajo Layout la clase lo hace por nosotros y utilizando sus métodos hacemos paginación.

Problema

El problema con TextView es que la parte visible del texto se puede cortar verticalmente en algún lugar en el medio de la última línea visible. En cuanto a dicho, debemos romper una nueva página cuando se cumple la última línea que encaja completamente en la altura de una vista.

Algoritmo

  • Repetimos las líneas de texto y verificamos si la línea bottom excede la altura de la vista;
  • Si es así, rompemos una página nueva y calculamos un nuevo valor para la altura acumulativa para comparar las siguientes líneas ' bottomcon (ver la implementación) El nuevo valor se define como top valor (línea roja en la imagen de abajo) de la línea que no encaja en la página anterior + TextView's altura.

enter image description here

Implementación

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

Nota 1

El algoritmo funciona no solo para TextView (Pagination usos de clase TextView's parámetros en la implementación anterior). Puede pasar cualquier conjunto de parámetros StaticLayout acepta y luego usa los diseños paginados para dibujar texto en Canvas/Bitmap/PdfDocument.

También puedes usar Spannable como yourText parámetro para diferentes fuentes, así como Html-cadenas formateadas (como en el ejemplo de abajo)

Nota 2

Cuando todo el texto tiene el mismo tamaño de fuente, todas las líneas tienen la misma altura. En ese caso, es posible que desee considerar una mayor optimización del algoritmo mediante el cálculo de una cantidad de líneas que se ajusta en una sola página y saltando a la línea correcta en cada iteración de bucle.


Muestra

La muestra a continuación pagina una cadena que contiene ambos html y Spanned texto.

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

Activitydiseño de s:

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

Captura de pantalla: enter image description here


26
2017-08-19 13:27



Echa un vistazo a mi proyecto de demostración.

La "magia" está en este código:

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

6
2017-08-20 09:11



Sorprendentemente encontrando bibliotecas para Paginación es difícil. Creo que es mejor usar otro elemento de la IU de Android además de TextView. Qué tal si WebView? Un ejemplo @ android-webview-example. Fragmento de código:

webView = (WebView) findViewById(R.id.webView1);

String customHtml = "<html><body><h1>Hello, WebView</h1></body></html>";
webView.loadData(customHtml, "text/html", "UTF-8");

Nota: Esto simplemente carga datos en un WebView, similar a un navegador web. Pero no nos detengamos solo con esta idea. Agregue esta interfaz de usuario para usar la paginación de WebViewClient onPageFinished . Por favor, lea el enlace SO @ html-book-like-pagination. Fragmento de código de una de las mejores respuestas de Dan:

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

Notas:

  • El código carga más datos al desplazarse por la página.
  • En la misma página web, hay una respuesta publicada por Engin Kurutepe para establecer las medidas de WebView. Esto es necesario para especificar una página en la paginación.

No he implementado la paginación, pero creo que este es un buen comienzo y muestra una promesa, debe ser rápido. Como puede ver, hay desarrolladores que implementaron esta característica.


4
2017-08-18 02:27