Paginating texto en Android

Estoy escribiendo un texto simple / visor de libros electrónicos para Android, por lo que he utilizado un TextView para mostrar el texto con formato HTML a los usuarios, para que puedan navegar por el texto en 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 apropiados de los algoritmos de salto de línea y 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 rest 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:

  • Cómo paginar texto largo en páginas en Android?
  • Ebook reader pagination issue en android
  • Texto de paginación basado en el tamaño del texto prestado

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:

  • ¿Cómo dividir el texto con estilo en páginas de Android?

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

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

captura de pantalla del lector de libros electrónicos PageTurner


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 ), así que no estoy buscando una respuesta óptima, sino una solución bastante rápida que pueda ser utilizada por el usuarios.


Actualización: 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?

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

Fondo

Lo que sabemos sobre el procesamiento de texto en TextView es que rompe un texto por líneas de acuerdo con el ancho de una vista. Al TextView's fonts TextView's podemos ver que el procesamiento del texto se realiza mediante la clase Layout . Así que podemos hacer uso del trabajo que la clase Layout hace por nosotros y utilizando sus métodos do pagination.

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

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

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

Nota 1

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

También puede usar Spannable como yourText parámetro de yourText para diferentes tipos de letra, así como también cadenas formateadas en Html ( como en el ejemplo a continuación ).

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

El ejemplo siguiente pagina una cadena que contiene tanto html como texto 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); } } 

Diseño de la actividad:

        

Captura de pantalla: enter image description here

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

Sorprendentemente, encontrar 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 WebView ? Un ejemplo @ android-webview-example . Fragmento de código:

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

Hello, WebView

"; 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 a la paginación mediante WebViewClient onPageFinished . 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.