PdfViewerFragment는 Android 애플리케이션 내에서 PDF 문서를 표시하는 데 사용할 수 있는 특수 Fragment입니다.
PdfViewerFragment를 사용하면 PDF 렌더링이 간소화되어 앱 기능의 다른 측면에 집중할 수 있습니다.
결과
버전 호환성
PdfViewerFragment를 사용하려면 애플리케이션이 최소 Android S(API 수준 31) 및 SDK 확장 프로그램 수준 13을 타겟팅해야 합니다. 이러한 호환성 요구사항을 충족하지 않으면 라이브러리에서 UnsupportedOperationException이 발생합니다.
SdkExtensions 모듈을 사용하여 런타임에 SDK 확장 프로그램 버전을 확인할 수 있습니다. 이렇게 하면 기기가 필요한 요구사항을 충족하는 경우에만 조건부로 프래그먼트와 PDF 문서를 로드할 수 있습니다.
if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
// Load the fragment and document.
}
종속 항목
PDF 뷰어를 애플리케이션에 통합하려면 앱의 모듈 build.gradle 파일에 androidx.pdf 종속 항목을 선언하세요. PDF 라이브러리는 Google Maven 저장소에서 액세스할 수 있습니다.
dependencies {
val pdfVersion = "1.0.0-alpha0X"
implementation("androidx.pdf:pdf:pdf-viewer-fragment:$pdfVersion")
}
PdfViewerFragment 기능
PdfViewerFragment는 PDF 문서를 페이지로 나누어 표시하므로 쉽게 탐색할 수 있습니다. 효율적인 로드를 위해 프래그먼트는 페이지 크기를 점진적으로 로드하는 2패스 렌더링 전략을 사용합니다.
메모리 사용량을 최적화하기 위해 PdfViewerFragment는 현재 표시되는 페이지만 렌더링하고 화면에 표시되지 않는 페이지의 비트맵은 해제합니다.
또한 PdfViewerFragment에는 문서 URI가 포함된 암시적 android.intent.action.ANNOTATE 인텐트를 실행하여 주석을 지원하는 플로팅 작업 버튼 (FAB)이 포함됩니다.
구현
Android 애플리케이션에 PDF 뷰어를 추가하는 것은 여러 단계로 구성된 프로세스입니다.
활동 레이아웃 만들기
PDF 뷰어를 호스팅하는 활동의 레이아웃 XML을 정의하는 것으로 시작합니다. 레이아웃에는 PdfViewerFragment 및 문서 내 검색과 같은 사용자 상호작용을 위한 버튼을 포함하는 FrameLayout가 포함되어야 합니다.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/pdf_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<FrameLayout
android:id="@+id/fragment_container_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/search_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/search_string"
app:strokeWidth="1dp"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
활동 설정
PdfViewerFragment를 호스팅하는 활동은 AppCompatActivity를 확장해야 합니다. 활동의 onCreate() 메서드에서 콘텐츠 뷰를 만든 레이아웃으로 설정하고 필요한 UI 요소를 초기화합니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val getContentButton: MaterialButton = findViewById(R.id.launch_button)
val searchButton: MaterialButton = findViewById(R.id.search_button)
}
}
PdfViewerFragment 초기화
getSupportFragmentManager()에서 가져온 프래그먼트 관리자를 사용하여 PdfViewerFragment 인스턴스를 만듭니다. 특히 구성 변경 중에 프래그먼트의 인스턴스가 이미 있는지 확인한 후 새 인스턴스를 만듭니다.
다음 예에서는 initializePdfViewerFragment() 함수가 프래그먼트 트랜잭션의 생성과 커밋을 처리합니다. 이 함수는 컨테이너의 기존 프래그먼트를 PdfViewerFragment 인스턴스로 바꿉니다.
class MainActivity : AppCompatActivity() {
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
private var pdfViewerFragment: PdfViewerFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
// ...
if (pdfViewerFragment == null) {
pdfViewerFragment =
supportFragmentManager
.findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG) as PdfViewerFragment?
}
}
// Used to instantiate and commit the fragment.
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
private fun initializePdfViewerFragment() {
// This condition can be skipped if you want to create a new fragment every time.
if (pdfViewerFragment == null) {
val fragmentManager: FragmentManager = supportFragmentManager
// Fragment initialization.
pdfViewerFragment = PdfViewerFragmentExtended()
val transaction: FragmentTransaction = fragmentManager.beginTransaction()
// Replace an existing fragment in a container with an instance of a new fragment.
transaction.replace(
R.id.fragment,4_container_view,
pdfViewerFragment!!,
PDF_VIEWER_FRAGMENT_TAG
)
transaction.commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
}
}
companion object {
private const val MIME_TYPE_PDF = "application/pdf"
private const val PDF_VIEWER_FRAGMENT_TAG = "pdf_viewer_fragment_tag"
}
}
PdfViewerFragment 기능 확장
PdfViewerFragment는 기능을 확장하기 위해 재정의할 수 있는 공개 함수를 노출합니다. PdfViewerFragment에서 상속받는 새 클래스를 만듭니다. 서브클래스에서 onLoadDocumentSuccess() 및 onLoadDocumentError()과 같은 메서드를 재정의하여 측정항목 로깅과 같은 맞춤 로직을 추가합니다.
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
class PdfViewerFragmentExtended : PdfViewerFragment() {
private val someLogger : SomeLogger = // ... used to log metrics
override fun onLoadDocumentSuccess() {
someLogger.log(/** log document success */)
}
override fun onLoadDocumentError(error: Throwable) {
someLogger.log(/** log document error */, error)
}
}
문서 검색 사용 설정
PdfViewerFragment에는 기본 검색 메뉴가 포함되어 있지 않지만 검색창은 지원됩니다. isTextSearchActive API를 사용하여 검색창의 표시 여부를 제어합니다. 문서 검색을 사용 설정하려면 PdfViewerFragment 인스턴스의 isTextSearchActive 속성을 설정합니다.
WindowCompat.setDecorFitsSystemWindows()를 사용하여 WindowInsetsCompat가 콘텐츠 뷰에 올바르게 전달되도록 합니다. 이는 검색 뷰의 올바른 위치 지정에 필요합니다.
class MainActivity : AppCompatActivity() {
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
override fun onCreate(savedInstanceState: Bundle?) {
// ...
searchButton.setOnClickListener {
pdfViewerFragment?.isTextSearchActive =
pdfViewerFragment?.isTextSearchActive == false
}
// Ensure WindowInsetsCompat are passed to content views without being
// consumed by the decor view. These insets are used to calculate the
// position of the search view.
WindowCompat.setDecorFitsSystemWindows(window, false)
}
}
파일 선택 도구와 통합
사용자가 기기에서 PDF 파일을 선택하도록 허용하려면 Android 파일 선택기와 PdfViewerFragment를 통합하세요. 먼저 활동의 레이아웃 XML을 업데이트하여 파일 선택기를 실행하는 버튼을 포함합니다.
<...>
<FrameLayout
...
app:layout_constraintBottom_toTopOf="@+id/launch_button"/>
// Adding a button to open file picker.
<com.google.android.material.button.MaterialButton
android:id="@+id/launch_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/launch_string"
app:strokeWidth="1dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/search_button"/>
<com.google.android.material.button.MaterialButton
...
app:layout_constraintStart_toEndOf="@id/launch_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
그런 다음 활동에서 registerForActivityResult(GetContent())를 사용하여 파일 선택기를 실행합니다. 사용자가 파일을 선택하면 콜백이 URI를 제공합니다. 그런 다음 이 URI를 사용하여 PdfViewerFragment 인스턴스의 documentUri 속성을 설정하여 선택한 PDF를 로드하고 표시합니다.
class MainActivity : AppCompatActivity() {
// ...
private var filePicker: ActivityResultLauncher<String> =
registerForActivityResult(GetContent()) { uri: Uri? ->
uri?.let {
initializePdfViewerFragment()
pdfViewerFragment?.documentUri = uri
}
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
getContentButton.setOnClickListener { filePicker.launch(MIME_TYPE_PDF) }
}
private fun initializePdfViewerFragment() {
// ...
}
companion object {
private const val MIME_TYPE_PDF = "application/pdf"
// ...
}
}
UI 맞춤설정
라이브러리에서 노출하는 XML 속성을 재정의하여 PdfViewerFragment의 사용자 인터페이스를 맞춤설정할 수 있습니다. 이렇게 하면 스크롤바, 페이지 표시기와 같은 요소의 모양을 앱의 디자인에 맞게 조정할 수 있습니다.
맞춤설정 가능한 속성은 다음과 같습니다.
fastScrollVerticalThumbDrawable- 스크롤바 썸의 드로어블을 설정합니다.fastScrollPageIndicatorBackgroundDrawable- 페이지 표시기의 배경 드로어블을 설정합니다.fastScrollPageIndicatorMarginEnd- 페이지 표시기의 오른쪽 여백을 설정합니다. 여백 값이 양수인지 확인합니다.fastScrollVerticalThumbMarginEnd- 세로 스크롤바 썸의 오른쪽 여백을 설정합니다. 여백 값이 양수인지 확인합니다.
이러한 맞춤설정을 적용하려면 XML 리소스에서 맞춤 스타일을 정의하세요.
<resources>
<style name="pdfContainerStyle">
<item name="fastScrollVerticalThumbDrawable">@drawable/custom_thumb_drawable</item>
<item name="fastScrollPageIndicatorBackgroundDrawable">@drawable/custom_page_indicator_background</item>
<item name="fastScrollVerticalThumbMarginEnd">8dp</item>
</style>
</resources>
그런 다음 PdfViewerFragment.newInstance(stylingOptions)로 프래그먼트 인스턴스를 만들 때 PdfStylingOptions를 사용하여 PdfViewerFragment에 맞춤 스타일 리소스를 제공합니다.
private fun initializePdfViewerFragment() {
// This condition can be skipped if you want to create a new fragment every time.
if (pdfViewerFragment == null) {
val fragmentManager: FragmentManager = supportFragmentManager
// Create styling options.
val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)
// Fragment initialization.
pdfViewerFragment = PdfViewerFragment.newInstance(stylingOptions)
// Execute fragment transaction.
}
}
PdfViewerFragment를 서브클래싱한 경우 보호된 생성자를 사용하여 스타일 옵션을 제공합니다. 이렇게 하면 확장된 프래그먼트에 맞춤 스타일이 올바르게 적용됩니다.
class StyledPdfViewerFragment: PdfViewerFragment {
constructor() : super()
private constructor(pdfStylingOptions: PdfStylingOptions) : super(pdfStylingOptions)
companion object {
fun newInstance(): StyledPdfViewerFragment {
val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)
return StyledPdfViewerFragment(stylingOptions)
}
}
}
구현 완료
다음 코드에서는 초기화, 파일 선택기 통합, 검색 기능, UI 맞춤설정을 비롯하여 활동에서 PdfViewerFragment를 구현하는 방법을 보여주는 전체 예를 제공합니다.
class MainActivity : AppCompatActivity() {
private var pdfViewerFragment: PdfViewerFragment? = null
private var filePicker: ActivityResultLauncher<String> =
registerForActivityResult(GetContent()) { uri: Uri? ->
uri?.let {
initializePdfViewerFragment()
pdfViewerFragment?.documentUri = uri
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (pdfViewerFragment == null) {
pdfViewerFragment =
supportFragmentManager
.findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG) as PdfViewerFragment?
}
val getContentButton: MaterialButton = findViewById(R.id.launch_button)
val searchButton: MaterialButton = findViewById(R.id.search_button)
getContentButton.setOnClickListener { filePicker.launch(MIME_TYPE_PDF) }
searchButton.setOnClickListener {
pdfViewerFragment?.isTextSearchActive = pdfViewerFragment?.isTextSearchActive == false
}
}
private fun initializePdfViewerFragment() {
// This condition can be skipped if you want to create a new fragment every time.
if (pdfViewerFragment == null) {
val fragmentManager: FragmentManager = supportFragmentManager
// Create styling options.
// val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)
// Fragment initialization.
// For customization:
// pdfViewerFragment = PdfViewerFragment.newInstance(stylingOptions)
pdfViewerFragment = PdfViewerFragmentExtended()
val transaction: FragmentTransaction = fragmentManager.beginTransaction()
// Replace an existing fragment in a container with an instance of a new fragment.
transaction.replace(
R.id.fragment_container_view,
pdfViewerFragment!!,
PDF_VIEWER_FRAGMENT_TAG
)
transaction.commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
}
}
companion object {
private const val MIME_TYPE_PDF = "application/pdf"
private const val PDF_VIEWER_FRAGMENT_TAG = "pdf_viewer_fragment_tag"
}
}
코드에 관한 핵심 사항
- 프로젝트가 최소 API 수준 및 SDK 확장 프로그램 요구사항을 충족하는지 확인합니다.
PdfViewerFragment를 호스팅하는 활동은AppCompatActivity를 확장해야 합니다.PdfViewerFragment를 확장하여 맞춤 동작을 추가할 수 있습니다.- XML 속성을 재정의하여
PdfViewerFragment의 UI를 맞춤설정합니다.