web-dev-qa-db-fra.com

Paginer le texte dans Android

J'écris une simple visionneuse de texte/eBook pour Android, j'ai donc utilisé un TextView pour montrer le texte au format HTML aux utilisateurs, afin qu'ils puissent parcourir le texte dans les pages en allant et venant. Mais mon problème est que je ne peux pas paginer le texte dans Android.

Je ne peux pas (ou je ne sais pas comment) obtenir les commentaires appropriés des algorithmes de saut de ligne et de saut de page dans lesquels TextView utilise pour diviser le texte en lignes et pages. Ainsi, je ne peux pas comprendre où le contenu se termine dans l'affichage réel, de sorte que je continue à partir du reste de la page suivante. Je veux trouver un moyen de surmonter ce problème.

Si je sais quel est le dernier caractère peint à l'écran, je peux facilement mettre suffisamment de caractères pour remplir un écran, et sachant où la peinture a été terminée, je peux continuer à la page suivante. Est-ce possible? Comment?


Des questions similaires ont été posées plusieurs fois sur StackOverflow, mais aucune réponse satisfaisante n'a été fournie. Ce ne sont que quelques-uns d'entre eux:

Il y avait une seule réponse, qui semble fonctionner, mais elle est lente. Il ajoute des caractères et des lignes jusqu'à ce que la page soit remplie. Je ne pense pas que ce soit une bonne façon de faire un saut de page:

Plutôt que cette question, il arrive que lecteur eBook de PageTurner le fasse bien, bien que ce soit en quelque sorte lent.

screenshot of PageTurner eBook reader


PS: je ne suis pas confiné à TextView, et je sais que les algorithmes de saut de ligne et de saut de page peuvent être assez complexes ( comme dans TeX ), donc je ne cherche pas une réponse optimale, mais plutôt une réponse raisonnablement rapide solution utilisable par les utilisateurs.


Mise à jour: Cela semble être un bon début pour obtenir la bonne réponse:

Existe-t-il un moyen de récupérer le nombre ou la plage de lignes visibles d'un TextView?

Réponse: Après avoir terminé la mise en page du texte, il est possible de trouver le texte 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);

            }
        });
41
Ho1

NOUVELLE RÉPONSE

PagedTextView bibliothèque (dans Kotlin) résume l'algorithme de mensonge ci-dessous en étendant Android TextView. Le exemple d'application = montre l'utilisation de la bibliothèque.

Configuration

dependencies {
    implementation 'com.github.onikx:pagedtextview:0.1.3'
}

Utilisation

<com.onik.pagedtextview.PagedTextView
        Android:layout_width="match_parent"
        Android:layout_height="match_parent" />

ANCIENNE RÉPONSE

L'algorithme ci-dessous implémente la pagination du texte dans la séparation de TextView lui-même sans changement dynamique simultané des attributs TextView et des paramètres de configuration de l'algorithme.

Contexte

Ce que nous savons sur le traitement de texte dans TextView, c'est qu'il casse correctement un texte par lignes en fonction de la largeur d'une vue. En regardant les sources de TextView nous pouvons voir que le traitement de texte est effectué par la classe Layout . Nous pouvons donc utiliser le travail que la classe Layout fait pour nous et utiliser ses méthodes pour paginer.

Problème

Le problème avec TextView est que la partie visible du texte peut être coupée verticalement quelque part au milieu de la dernière ligne visible. En ce qui concerne ledit, nous devons casser une nouvelle page lorsque la dernière ligne qui correspond entièrement à la hauteur d'une vue est remplie.

Algorithme

  • Nous parcourons les lignes de texte et vérifions si le bottom de la ligne dépasse la hauteur de la vue;
  • Si c'est le cas, nous cassons une nouvelle page et calculons une nouvelle valeur pour la hauteur cumulée pour comparer les lignes suivantes 'bottom avec ( voir l'implémentation). La nouvelle valeur est définie comme top value ( ligne rouge dans l'image ci-dessous) de la ligne qui ne rentre pas dans la page précédente + TextView's la taille.

enter image description here

La mise en oeuvre

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

Note 1

L'algorithme ne fonctionne pas uniquement pour TextView (Pagination la classe utilise TextView's paramètres dans l'implémentation ci-dessus). Vous pouvez passer n'importe quel ensemble de paramètres StaticLayout accepte et utiliser plus tard les dispositions paginées pour dessiner du texte sur Canvas / Bitmap =/ PdfDocument .

Vous pouvez également utiliser Spannable en tant que paramètre yourText pour différentes polices ainsi que Html- formaté chaînes ( comme dans l'exemple ci-dessous).

Note 2

Lorsque tout le texte a la même taille de police, toutes les lignes ont la même hauteur. Dans ce cas, vous souhaiterez peut-être envisager une optimisation supplémentaire de l'algorithme en calculant un nombre de lignes qui tient dans une seule page et en sautant sur la ligne appropriée à chaque itération de boucle.


Échantillon

L'exemple ci-dessous pagine une chaîne contenant à la fois du texte html et 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);
    }
}

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

Capture d'écran: enter image description here

37
Onik

Jetez un oeil à mon projet de démonstration .

La "magie" est dans ce code:

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

Étonnamment, il est difficile de trouver des bibliothèques pour la pagination . Je pense qu'il est préférable d'utiliser un autre élément Android UI en plus de TextView. Que diriez-vous WebView ? Un exemple @ Android-webview-example . Extrait de code:

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

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

Remarque: Cela charge simplement les données sur une WebView, semblable à un navigateur Web. Mais ne nous arrêtons pas juste avec cette idée. Ajoutez cette interface utilisateur à l'utilisation de la pagination par WebViewClient onPageFinished . Veuillez lire sur SO link @ html-book-like-pagination . Extrait de code de l'une des meilleures réponses de Dan:

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

Remarques:

  • Le code charge plus de données lors du défilement de la page.
  • Sur la même page Web, il y a une réponse publiée par Engin Kurutepe pour définir les mesures pour le WebView. Ceci est nécessaire pour spécifier une page en pagination.

Je n'ai pas implémenté la pagination mais je pense que c'est un bon début et semble prometteur, devrait être rapide. Comme vous pouvez le voir, certains développeurs ont implémenté cette fonctionnalité.

5