java android android-layout pagination android-textview textview

java - Paginar texto en Android



android-layout pagination (3)

Estoy escribiendo un simple visor de texto / eBook para Android, por lo que he usado 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 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 real, por lo que continúo con el resto en la página siguiente. Quiero encontrar la manera 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 se terminó la pintura real, puedo continuar en la página siguiente. es posible? ¿Cómo?

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

Hubo una sola respuesta, 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 manera de hacer saltos de página:

En lugar de esta pregunta, sucede que el lector de eBooks PageTurner lo hace bien, aunque de alguna manera es lento.

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 una solución razonablemente rápida que pueda ser utilizada por el usuarios.

Actualización: este parece ser un buen comienzo para obtener la respuesta correcta:

¿Hay alguna manera de recuperar el recuento de líneas visibles o el rango 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 dentro de TextView es que divide correctamente un texto por líneas de acuerdo con el ancho de una vista. Mirando las fuentes de TextView podemos ver que el procesamiento de texto lo realiza la clase Layout . Entonces podemos hacer uso del trabajo que la clase Layout hace por nosotros y utilizar sus métodos para hacer paginación.

Problema

El problema con TextView es que la parte visible del texto podría cortarse verticalmente en algún lugar en el medio de la última línea visible. Con respecto a lo dicho, deberíamos romper una nueva página cuando se cumpla la última línea que se ajusta completamente a la altura de una vista.

Algoritmo

  • Repetimos 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 acumulativa para comparar la bottom las siguientes líneas ( 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 altura de TextView''s página anterior + TextView''s .

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<>(); 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 utiliza TextView''s parámetros TextView''s en la implementación anterior). Puede pasar cualquier conjunto de parámetros que StaticLayout acepte y luego usar los diseños paginados para dibujar texto en Canvas / Bitmap / PdfDocument .

También puede usar Spannable como parámetro de texto para diferentes fuentes, así como cadenas con formato 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 calculando una cantidad de líneas que encajen en una sola página y saltando a la línea adecuada en cada iteración del bucle.

Muestra

El ejemplo a continuación pagina una cadena que contiene html y 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 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(v -> { mCurrentIndex = (mCurrentIndex > 0) ? mCurrentIndex - 1 : 0; update(); }); findViewById(R.id.btn_forward).setOnClickListener(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:

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


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


Sorprendentemente, encontrar bibliotecas para la paginación es difícil. Creo que es mejor usar otro elemento de la interfaz de usuario de Android además de TextView. ¿Qué hay de 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 IU para usar la paginación por WebViewClient en PageFinished . Lea en SO link @ 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 mediciones para el WebView. Esto es necesario para especificar una página en paginación.

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