Computer >> 컴퓨터 >  >> 프로그래밍 >> Android

안드로이드에서 페이지 넘김(Pagination) 텍스트 구현하는 방법

이 튜토리얼에서는 안드로이드 앱에서 긴 텍스트를 화면 크기에 맞춰 여러 페이지로 나누어 보여주는 페이지 넘김(Pagination) 기능을 구현하는 방법을 단계별로 살펴봅니다. 전자책 리더나 긴 문서를 다루는 앱을 만들 때 유용하게 활용할 수 있는 기법입니다.

핵심 원리

페이지 넘김의 핵심은 StaticLayout 클래스입니다. TextView에 실제로 렌더링하기 전에 텍스트가 차지하는 줄 수와 각 줄의 위치를 미리 계산하여, 화면 높이를 초과하는 지점마다 텍스트를 잘라 새로운 페이지로 분리하는 방식으로 동작합니다.

1단계 — 새 프로젝트 생성

안드로이드 스튜디오에서 File ⇒ New Project를 선택하고, 필요한 정보를 모두 입력하여 새 프로젝트를 생성합니다.

2단계 — 레이아웃 작성

res/layout/activity_main.xml 파일에 아래 코드를 추가합니다. 이전/다음 페이지로 이동할 두 개의 버튼과 텍스트를 표시할 TextView로 구성되어 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="16dp"
    android:paddingTop="16dp"
    android:paddingRight="16dp"
    android:paddingBottom="16dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:id="@+id/btn_back"
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@android:color/transparent" />
        <Button
            android:id="@+id/btn_forward"
            style="?android:attr/buttonBarButtonStyle"
            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>

3단계 — MainActivity 구현

src/MainActivity.java 파일에 아래 코드를 추가합니다. HTML 문자열과 Spannable 문자열을 결합한 텍스트를 준비하고, 뷰의 레이아웃이 완료된 시점에 Pagination 객체를 생성합니다.

package app.com.sample;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
    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_main);
        mTextView = 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() {
                // 중복 호출을 방지하기 위해 리스너 제거
                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 view) {
                mCurrentIndex = (mCurrentIndex > 0) ? mCurrentIndex - 1 : 0;
                update();
            }
        });
        findViewById(R.id.btn_forward).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                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);
    }
}

코드 포인트 정리

  • ViewTreeObserver.OnGlobalLayoutListener: TextView의 실제 가로·세로 크기가 확정된 후에 Pagination을 초기화해야 하므로 전역 레이아웃 리스너를 사용합니다.
  • mCurrentIndex: 현재 보고 있는 페이지 번호를 추적하며, 버튼 클릭 시 범위를 벗어나지 않도록 경계 검사를 수행합니다.
  • update(): 현재 인덱스에 해당하는 페이지 텍스트를 가져와 TextView에 설정합니다.

4단계 — Pagination 클래스 작성

src/Pagination.java 파일에 아래 코드를 추가합니다. 이 클래스가 페이지 분할의 핵심 로직을 담당합니다.

package app.com.sample;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import java.util.ArrayList;
import java.util.List;
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;
    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)) {
                // 레이아웃 높이를 초과한 경우 새 페이지 시작
                addPage(text.subSequence(startOffset, layout.getLineStart(i)));
                startOffset = layout.getLineStart(i);
                height = layout.getLineTop(i) + mHeight;
            }
            if (i == lines - 1) {
                // 남은 텍스트를 마지막 페이지에 배치
                addPage(text.subSequence(startOffset, layout.getLineEnd(i)));
                return;
            }
        }
    }
    private void addPage(CharSequence text) {
        mPages.add(text);
    }
    int size() {
        return mPages.size();
    }
    CharSequence get(int index) {
        return (index >= 0 && index < mPages.size()) ? mPages.get(index) : null;
    }
}

Pagination 클래스 동작 방식

  1. 전체 텍스트로 StaticLayout을 생성하여 각 줄의 위치 정보를 얻습니다.
  2. 줄의 하단(bottom) 좌표가 사용 가능한 높이를 초과하면, 그 줄의 시작 지점부터 새 페이지를 만듭니다.
  3. 마지막 줄에 도달하면 남은 텍스트 전체를 마지막 페이지로 추가하고 종료합니다.

5단계 — 매니페스트 설정

androidManifest.xml 파일에 아래 코드를 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="app.com.sample">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

앱 실행 및 결과 확인

이제 애플리케이션을 실행해 보겠습니다. 실제 안드로이드 기기를 컴퓨터에 연결했다고 가정합니다. 안드로이드 스튜디오에서 프로젝트의 액티비티 파일 중 하나를 연 뒤, 툴바의 Run 아이콘을 클릭하세요. 실행 옵션에서 자신의 모바일 기기를 선택하면, 기기 화면에 아래와 같은 결과가 표시됩니다.

안드로이드에서 페이지 넘김(Pagination) 텍스트 구현하는 방법

안드로이드에서 페이지 넘김(Pagination) 텍스트 구현하는 방법

마무리

이처럼 StaticLayout을 활용하면 TextView의 크기와 폰트 속성을 그대로 반영하여 텍스트를 정확한 페이지 단위로 분할할 수 있습니다. 화면 회전이나 폰트 크기 변경에 대응하려면 레이아웃 변경 시점에 Pagination 객체를 다시 생성해 주면 됩니다. 전자책 앱이나 문서 뷰어를 개발할 때 이 예제를 응용해 보세요.