PDF no Android

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /PDF no Android

PDF no Android

Vinícius Thiengo18/07/2017
(2106) (8) (134) (21)
Go-ahead
"Com tudo o que aconteceu com você, você pode sentir pena de si mesmo ou tratar o que aconteceu como um presente. Tudo é tanto uma oportunidade de crescer ou um obstáculo para parar de crescer. Você tem que escolher."
Wayne W. Dyer
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas+ 144
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas934
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Acessar Livro
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, tudo bem?

Neste artigo vamos trabalhar a apresentação de arquivos PDF em aplicativos Android. O gerenciamento de apresentação inclui controle de: zoom; tipo de leitura (interna ao aplicativo ou externa, SDCard); listeners; posicionamento de scroll; e outros.

Depois da apresentação da API PdfViewer, vamos a construção de um aplicativo de documentações de linguagens de programação onde o usuário poderá escolher qual documentação estudar e também voltar de onde parou, última página aberta, para prosseguir com o estudo:

Como nos últimos projetos estudados aqui no Blog, neste também prosseguiremos com a linguagem Kotlin. Caso ainda não a conheça, veja primeiro o artigo, com vídeo, a seguir: Kotlin Android, Entendendo e Primeiro Projeto.

Caso queira ir direto ao vídeo deste conteúdo de PdfViewer, basta acessar a seção Vídeo com implementação passo a passo da biblioteca.

A seguir os tópicos que estaremos abordando:

Biblioteca Android PdfViewer

Optei por utilizar esta biblioteca por ela ser a de melhor ranking entre as libraries PDF Android na comunidade de desenvolvedores.

Apesar da PdfViewer API ser somente para leitura de arquivos PDF, ela é bem robusta em comparação a outras bibliotecas, além de nos permite o trabalho com vários listeners.

A library funciona a partir do Android API 11, Honeycomb, e desde a criação, em 2016, passou por inúmeras atualizações.

Essas modificações na API, como já informado em outros artigos aqui do Blog, passa segurança aos desenvolvedores que consomem ela, pois sabemos que as issues estão sendo respondidas e a library evoluída.

Um ponto negativo da library é que quando adicionada ao projeto, o APK Android fica com mais 16MB.

Para acesso a documentação completa, entre no GitHub da Biblioteca em: https://github.com/barteksc/AndroidPdfViewer.

Configuração e inicialização da API

A configuração de referência necessária é no Gradle App Level, ou build.gradle (Module: app):

...
dependencies {
compile 'com.github.barteksc:android-pdf-viewer:2.6.1'
}
...

 

Na época da construção deste artigo, a versão estável e mais atual da API era a versão 2.6.1.

Para inicialização temos de utilizar a View da API e o carregamento dela em alguma classe do aplicativo que permita acesso a View:

...
<com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
...

 

A seguir o acesso a View, que apesar de estarmos colocando dentro do onCreate() de uma atividade, você pode colocar onde for necessário em seu projeto, desde que consiga acessar a instância do PDFView:

class MainActivity : AppCompatActivity(){

override fun onCreate(savedInstanceState: Bundle?) {
...

pdfView
.fromAsset( "algum-arquivo-pdf-dentro-do-assets-folder.pdf" )
.load()
}
}

 

No código acima utilizamos a versão de abertura de arquivo PDF que se encontra no /assets do projeto, o método fromAsset(). Na próxima seção vamos listar outros métodos de abertura.

Métodos de abertura de arquivo

A seguir todos os métodos presentes na API PdfViewer para abertura de arquivos PDF, métodos também conhecidos como providers:

...
/* PDF em SDCard ou em algum folder interno ao aplicativo */
pdfView.fromUri(Uri).load()

/* PDF em SDCard */
pdfView.fromFile(File).load()

/*
* PDF que está em formato de array de bytes, algo comum
* depois de um download de arquivo direto de um servidor
* remoto
* */
pdfView.fromBytes(byte[]).load()

/*
* PDF que está em formato de stream, como na versão acima:
* algo comum de ocorrer quando se realizou o download do
* arquivo de alguma fonte remota.
* */
pdfView.fromStream(InputStream).load()

/*
* Permite que o programador crie o próprio provider dele
* para abertura / leitura de arquivos PDF. Para isso é
* necessário implementar a Interface DocumentSource da API.
* */
pdfView.fromSource(DocumentSource).load()

/* PDF que está no folder /assets do projeto */
pdfView.fromAsset(String).load()
...

 

Para acesso a interface de criação de provider, entre em: DocumentSource.

Não é possível carregar um arquivo remoto diretamente com algum método da API?

Ao menos até a construção deste artigo, não. Não era possível. E essa é uma característica já solicitada aos mantenedores da API e a resposta foi, aparentemente, definitiva, informando que essa melhoria não seria incluída, pois deixaria a biblioteca ainda maior, tendo em mente que ela já acrescenta em torno de 16MB ao APK final.

O que possivelmente ocorrerá, segundo documentação, é a criação de uma outra API somente para carregamento de PDFs remotos.

Antes de prosseguir, saiba que o carregamento de arquivos PDF que estão no SDCard exige a permissão:

...
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...

 

Lembrando que esta é uma dangerous permission e em devices com o Android API 23+ a solicitação deve ocorrer em tempo de execução, como explicado no artigo Sistema de Permissões em Tempo de Execução, Android M.

Listeners

A seguir vamos a listagem dos listeners disponíveis até a versão 2.6.1.

Abaixo o código do listener de término de carregamento de PDF, OnLoadCompleteListener:

class PdfActivity : AppCompatActivity(), OnLoadCompleteListener {
override fun onCreate(savedInstanceState: Bundle?) {
...
pdfView
.fromAsset( assetsString )
.onLoad( this )
.load()
}

override fun loadComplete( numeroDePaginsPdf: Int ) {
/* TODO */
}
}

 

A seguir o código do listener de mudança de página, OnPageChangeListener:

class PdfActivity : AppCompatActivity(), OnPageChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
...
pdfView
.fromAsset( assetsString )
.onPageChange( this )
.load()
}

override fun onPageChanged( numeroPaginaAtual: Int, numeroTotalPaginas: Int ) {
/* TODO */
}
}

 

Agora o código do listener de início de renderização, OnRenderListener:

class PdfActivity : AppCompatActivity(), OnRenderListener {
override fun onCreate(savedInstanceState: Bundle?) {
...
pdfView
.fromAsset( assetsString )
.onRender( this )
.load()
}

override fun onInitiallyRendered(
numeroTotalPaginas: Int,
larguraPagina: Float,
alturaPagina: Float ) {
/* TODO */
}
}

 

O código do listener de scroll de páginas, OnPageScrollListener:

class PdfActivity : AppCompatActivity(), OnPageScrollListener {
override fun onCreate(savedInstanceState: Bundle?) {
...
pdfView
.fromAsset( assetsString )
.onPageScroll( this )
.load()
}

override fun onPageScrolled(paginaAtual: Int, posicaoOffset: Float) {
/* TODO */
}
}

 

O listener de mudança de página que permite o desenho de conteúdo na página atual, OnDrawListener:

class PdfActivity : AppCompatActivity(), OnDrawListener {
override fun onCreate(savedInstanceState: Bundle?) {
...
pdfView
.fromAsset( assetsString )
.onDraw( this )
.load()
}

override fun onLayerDrawn(
canvas: Canvas?,
larguraPagina: Float,
alturaPagina: Float,
paginaAtual: Int) {

/* TODO */
}
}

 

Por fim o código do listener que permite o trabalho quando houver error de carregamento, OnErrorListener:

class PdfActivity : AppCompatActivity(), OnErrorListener {
override fun onCreate(savedInstanceState: Bundle?) {
...
pdfView
.fromAsset( assetsString )
.onError( this )
.load()
}

override fun onError( t: Throwable? ) {
/* TODO */
}
}

 

Todos os listeners podem ser utilizados em conjunto, incluindo o uso dos outros providers, e não somente o fromAsset().

Na versão beta, 2.7.0-beta, tem também o listener que permite o desenho de algo em todas as páginas do PDF, utilizando no caso o método onDrawAll() que não está disponível abaixo da versão 2.7.0-beta.

Métodos úteis

A seguir a listagem e as descrições de uso dos métodos úteis da API PdfViewer:

...
pdfView
.fromAsset( assetsString )

/*
* Permite a definição da página que será carregada inicialmente.
* A contagem inicia em 0.
* */
.defaultPage( doc?.getActualPage(this) ?: 0 )

/*
* Caso um ScrollHandle seja definido, a numeração da página estará
* presente na tela para que o usuário saiba em qual página está,
* isso sem necessidade de dar o zoom nela. É possível implementar
* o seu próprio ScrollHandle, mas a API também já fornece uma
* implementação que tem como parâmetro um objeto de contexto,
* DefaultScrollHandle.
* */
.scrollHandle( DefaultScrollHandle(this) )

/*
* Se definido como false, o usuário não conseguirá mudar de página.
* */
.enableSwipe(true)

/*
* Por padrão o swipe é vertical, ou seja, as próximas páginas estão
* abaixo no scroll. Com swipeHorizontal() recebendo true o swipe
* passa a ser horizontal, onde a próxima página é a que está a direita.
* */
.swipeHorizontal(true)

/*
* Útil para PDFs que necessitam de senha para serem visualizados.
* */
.password(null)

/*
* Caso true, permite que os níveis de zoom (min, middle, max) também
* seja acionados caso o usuário dê touchs na tela do device.
* */
.enableDoubletap(true)

/*
* Caso true, permite que anotações e comentários, extra PDF original,
* sejam apresentados.
* */
.enableAnnotationRendering(true)

/*
* Caso true, permite que haja otimização de renderização em telas
* menores.
* */
.enableAntialiasing(true)

/*
* Permite definir quais páginas do PDF serão acessíveis, iniciando
* a contagem em 0. Por padrão todas as páginas são acessíveis.
* */
.pages(0, 2,4)

/*
* Método adicionado a partir da API versão 2.7.0-beta. Tem como
* função colocar espaço, em dp, entre as páginas do PDF.
* */
.spacing( 10 )
.load()

/*
* Os métodos de controle de como serão os zooms no PDF. É possível
* definir três níveis, todos no tipo float, por isso a necessidade
* do F ao final do argumento. Por padrão os níveis iniciais são:
* 1, 1.75 e 3.
* */
pdfView.setMinZoom(1F)
pdfView.setMidZoom(1.75F)
pdfView.setMaxZoom(3F)
...

 

Com isso podemos partir para o projeto de exemplo.

Projeto Android de exemplo

Nosso projeto de exemplo será um aplicativo de documentações de linguagem de programação que, acredite, você até mesmo poderá utiliza-lo para colocar em sua conta da Play Store para assim aumentar seu portfólio ou então conseguir alguns ganhos com a API de anúncios que você integrar ao aplicativo.

Vamos trabalhar o projeto de exemplo em duas partes:

  • A primeira parte é onde teremos o projeto simples e inicial, ainda sem os arquivos PDFs e sem a integração com a API PdfViewer;
  • Na segunda onde iremos colocar não somente a API em estudo e os arquivos PDF, mas também outros algoritmos para melhorar o uso do aplicativo.

O acesso completo a primeira parte do projeto, além de estar presente aqui no artigo, você tem no seguinte GitHub: https://github.com/viniciusthiengo/dot-documentacoes-inicial.

Já a parte final, você tem no GitHub: https://github.com/viniciusthiengo/dot-documentacoes-final. Recapitulando que ambas as partes estão explicadas por completo no artigo, então não deixe de segui-lo até o final.

Para prosseguir, em seu Android Studio crie um novo projeto Android Kotlin com uma Empty Activity e com o nome "Dot.Documentações".

Ao final desta primeira parte teremos um aplicativo simples como abaixo:

E a seguinte estrutra de projeto:

Note que para ter acesso as imagens do aplicativo, basta entrar nos folders /drawable em qualquer um dos GitHubs disponibilizados: Folder /res projeto versão final.

Configurações Gradle

A seguir as configurações do Gradle Project Level, ou build.gradle (Project: DotDocumentacoes):

buildscript {
ext.kotlin_version = '1.1.3-2'
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Note que neste arquivo somente temos as configurações extras referentes a um projeto Kotlin. E ele permanecerá assim até o final do projeto.

Agora as configurações do Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 26
buildToolsVersion "26.0.0"
defaultConfig {
applicationId "br.com.thiengo.pdfviwertest"
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
testCompile 'junit:junit:4.12'

/* PARA TRABALHO COMPLETO COM O FRAMEWORK RECYCLERVIEW */
compile 'com.android.support:recyclerview-v7:26.+'

/* PARA TRABALHO COMPLETO COM O CARDVIEW */
compile 'com.android.support:cardview-v7:26.+'

/* PARA ACESSO A VIEWS COMO: COORDINATORLAYOUT, APPBARLAYOUT E TOOLBAR */
compile 'com.android.support:design:26.+'

compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
mavenCentral()
}

 

No Gradle acima, além das configurações referentes ao Kotlin, temos também algumas referências específicas ao CardView e ao RecyclerView para não termos problemas de inflate com os layouts. Na segunda parte voltaremos a está versão do Gradle para adicionarmos a referência a API PdfViewer.

Note que caso na época de sua implementação tenha versões mais atuais dos arquivos Gradle e também das APIs em uso, siga com as versões mais atuais, pois o projeto deverá rodar sem problemas.

Configurações AndroidManifest

A seguir as configurações iniciais do AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.pdfviwertest">

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

 

Neste arquivo voltaremos para adicionar a referência a uma atividade que criaremos na parte dois do projeto.

Configurações de estilo

Os arquivos de estilo são simples como quando na criação de um novo projeto Empty Activity no Android Studio. Vamos iniciar com o arquivo de definição de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#009688</color>
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#607D8B</color>
</resources>

 

Então o arquivo de String, /res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Dot.Documentações</string>
</resources>

 

E por fim o arquivo de definição de estilo, /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

 

Voltaremos a este último arquivo PDF na parte dois do projeto para a adição de temas que serão necessários na atividade que criaremos nessa parte do projeto.

Classe de domínio

No pacote /domain temos uma classe, Doc, referente as documentações que estaremos apresentando no aplicativo. Abaixo o código inicial desta classe:

class Doc(
val imageRes: Int,
val language: String,
val pagesNumber: Int)

 

Ainda voltaremos a está classe, na segunda parte do projeto, para uma série de atualizações para responder melhor ao nosso domínio do problema.

Camada de dados

A camada de dados, pacote /data, também é simples e mesmo com os objetos sendo criados nesta camada, ela não representa uma base mock, de dados simulados, isso, pois em nosso projeto de exemplo todos os conteúdos realmente serão internos ao app.

Segue código inicial da classe Database:

class Database {
companion object{
fun getDocs() = listOf(
Doc(R.drawable.kotlin_bg, "Kotlin", 194),
Doc(R.drawable.java_bg, "Java", 670),
Doc(R.drawable.python_bg, "Python", 1538),
Doc(R.drawable.haskell_bg, "Haskell", 503),
Doc(R.drawable.scala_bg, "Scala", 547)
)
}
}

 

Um companion object está sendo utilizado para que não seja necessária a criação de uma instância para acesso ao método getDocs(). A invocação será via sintaxe de membro estático.

Classe adaptadora

Neste projeto estaremos trabalhando com um framework de lista, mais precisamente, o RecyclerView. Como classe adaptadora teremos a DocAdapter.

A seguir o XML de layout de itens deste adapter, /res/layout/iten_doc.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/tools"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<android.support.v7.widget.CardView
android:id="@+id/cv_cover"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
card_view:cardBackgroundColor="@android:color/white">

<ImageView
android:id="@+id/iv_cover"
android:layout_width="155dp"
android:layout_height="75dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:contentDescription="Capa documentação"
android:scaleType="fitCenter" />
</android.support.v7.widget.CardView>

<TextView
android:id="@+id/tv_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_toEndOf="@+id/cv_cover"
android:layout_toRightOf="@+id/cv_cover"
android:textSize="18sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_total_pages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_language"
android:layout_alignStart="@+id/tv_language"
android:layout_below="@+id/tv_language"
android:layout_marginTop="6dp" />
</RelativeLayout>

 

Abaixo o diagrama do layout anterior:

E por fim o código Kotlin da classe DocAdapter:

class DocAdapter(
private val context: Context,
private val docList: List<Doc>) :
RecyclerView.Adapter<DocAdapter.ViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int) : DocAdapter.ViewHolder {

val v = LayoutInflater
.from(context)
.inflate(R.layout.iten_doc, parent, false)

return ViewHolder(v)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setData(docList[position])
}

override fun getItemCount(): Int {
return docList.size
}

inner class ViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {

var ivCover: ImageView
var tvLanguage: TextView
var tvTotalPages: TextView

init {
itemView.setOnClickListener(this)
ivCover = itemView.findViewById(R.id.iv_cover)
tvLanguage = itemView.findViewById(R.id.tv_language)
tvTotalPages = itemView.findViewById(R.id.tv_total_pages)
}

fun setData(doc: Doc) {
ivCover.setImageResource( doc.imageRes )
tvLanguage.text = doc.language
tvTotalPages.text = "${doc.pagesNumber} páginas"
}
}
}

 

Nada de mais, somente os códigos necessários para o trabalho com um adapter de um RecyclerView. Essa classe, como outras já apresentadas, também passará por atualizações na parte dois do projeto.

Atividade principal

A seguir o simples layout da atividade principal, /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rv_todo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="br.com.thiengo.pdfviwertest.MainActivity" />

 

Devido a simplicidade é dispensável a apresentação do diagrama do layout anterior. Podemos ir direto ao código Kotlin da MainActivity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

initRecycler()
}

private fun initRecycler() {
rv_todo.setHasFixedSize(true)

val mLayoutManager = LinearLayoutManager(this)
rv_todo.layoutManager = mLayoutManager

val divider = DividerItemDecoration( this, mLayoutManager.orientation )
rv_todo.addItemDecoration(divider)

val adapter = DocAdapter(this, Database.getDocs())
rv_todo.adapter = adapter
}
}

 

Assim finalizamos a parte um do projeto e podemos partir para as melhorias na parte dois.

Evoluindo o aplicativo

A seguir o que desejamos que o aplicativo faça:

  • Com o clique / touch em algum dos itens da lista, deverá ser aberto o PDF da documentação da linguagem do item acionado, isso em uma nova atividade;
  • O usuário terá de visualizar as páginas do PDF aberto, por padrão, em tela cheia, ou seja, uma página deve preencher por inteiro a tela, isso quando o device estiver em modo portrait ou landscape;
  • Caso o usuário saia do PDF de documentação, quando voltar a este, o PDF, deverá carregar na última página visualizada;
  • Nos itens de lista deverá também ser apresentado o número da última página lida pelo usuário, somente se ele estiver ainda na primeira página é que nada deverá ser mostrado no item.

Com isso vamos a evolução do aplicativo.

Colocando os arquivos PDF internamente no projeto

Em sua visualização de projeto do Android Studio, selecione a versão "Project" de visualização:

Logo depois expanda o projeto até o folder /main:

Assim clique com o botão direito neste folder, vá em "New", logo depois em "Directory". Por fim digite assets e clique em "Ok":

Estaremos trabalhando com cinco documentações neste projeto de exemplo. Os arquivos PDF delas podem ser acessados nos links a seguir:

Copie todos esses arquivos e cole dentro de seu novo folder /assets. Ao final terá algo como:

Atualização da camada de domínio

Na classe Doc em /domain, precisamos adicionar uma nova propriedade, uma referente ao path da documentação da linguagem no objeto Doc.

Atualize a classe como a seguir:

class Doc(
val path: String,
val imageRes: Int,
val language: String,
val pagesNumber: Int)

 

Essa nova propriedade será utilizada junto ao provider fromAsset() da API PdfViewer para acesso aos nossos PDFs no /assets.

Assim podemos atualizar a classe Database para inicializar corretamente os objetos Doc:

class Database {
companion object{
fun getDocs() = listOf(
Doc("kotlin-docs.pdf", R.drawable.kotlin_bg, "Kotlin", 194),
Doc("java-docs.pdf", R.drawable.java_bg, "Java", 670),
Doc("python-docs.pdf", R.drawable.python_bg, "Python", 1538),
Doc("haskell-docs.pdf", R.drawable.haskell_bg, "Haskell", 503),
Doc("scala-docs.pdf", R.drawable.scala_bg, "Scala", 547)
)
}
}

 

Não é necessário colocar "assets/" como prefixo no path?

Neste caso não, pois o provider que estaremos utilizando, fromAsset(), já tem o acesso a este folder. Caso os PDFs estivessem dentro de um outro folder, /pdf, por exemplo, dentro do /assets, ai sim teríamos de colocar este pdf/ como prefixo dos nomes dos arquivos, exemplo: "pdf/kotlin-docs.pdf".

Implementação da Interface Parcelable

Ainda não temos a atividade que será responsável por apresentar por completo o arquivo PDF enviado a ela, mas sabemos que na verdade o que será enviado a essa outra atividade será um objeto do tipo Doc que tem internamente o path do arquivo PDF.

Para que esse envio ocorra sem problemas via Intent, teremos de ter a classe Doc implementando a Interface Parcelable.

Vamos utilizar o plugin Parcelable Kotlin para isso. Caso ainda não o conheça, rapidamente pare com este artigo e vá ao conteúdo a seguir para saber como incorporar e utilizar esse plugin no Android Studio: Configurando o plugin gerador de código da API Parcelable.

Ao final da implementação do Parcelable teremos a classe Doc como a seguir:

class Doc(
val path: String,
val imageRes: Int,
val language: String,
val pagesNumber: Int) : Parcelable {

companion object {
@JvmField val DOC_KEY = "doc"

@JvmField val CREATOR: Parcelable.Creator<Doc> = object : Parcelable.Creator<Doc> {
override fun createFromParcel(source: Parcel): Doc = Doc(source)
override fun newArray(size: Int): Array<Doc?> = arrayOfNulls(size)
}
}

constructor(source: Parcel) : this(
source.readString(),
source.readInt(),
source.readString(),
source.readInt()
)

override fun describeContents() = 0

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(path)
dest.writeInt(imageRes)
dest.writeString(language)
dest.writeInt(pagesNumber)
}
}

 

Ok, a implementação do Parcelable eu entendi, mas para que a propriedade companion DOC_KEY?

Esta propriedade será utilizada como chave de acesso ao objeto que será enviado via Intent de uma atividade a outra. Assim não teremos valores mágicos sendo trabalhados no aplicativo, algo que atrasaria posteriores evoluções dele.

Atualização Gradle App Level para referência a PdfViewer

No Gradle App Level, ou build.gradle (Module: app), adicione a seguinte nova referência e logo depois sincronize o projeto:

...
dependencies {
...
/* PDF VIWER */
compile 'com.github.barteksc:android-pdf-viewer:2.6.1'
}
...

 

Caso na época que você esteja estudando este artigo haja uma versão estável mais atual que a versão 2.6.1 da API, utilize ela, pois mesmo assim o projeto deverá rodar sem problemas.

PdfActivity

Agora criaremos a atividade que será responsável pela abertura dos PDFs. Vamos iniciar atualizando o arquivo de estilo /res/values/styles.xml com alguns novos temas:

<?xml version="1.0" encoding="utf-8"?>
<resources>
...

<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

 

Assim podemos ir ao layout dessa nova atividade, /res/layout/activity_pdf.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="br.com.thiengo.pdfviwertest.PdfActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

 

Abaixo o diagrama do layout anterior:

A seguir o código inicial de abertura de PDF da atividade PdfActivity:

class PdfActivity : AppCompatActivity() {

var doc: Doc? = null
var toolbar: Toolbar? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pdf)

toolbar = findViewById(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
getSupportActionBar()?.setDisplayShowHomeEnabled(true)

doc = intent.getParcelableExtra( Doc.DOC_KEY )

pdfView
.fromAsset( doc?.path )
.defaultPage( 0 )
.scrollHandle( DefaultScrollHandle(this) )
.enableSwipe(true)
.swipeHorizontal(true)
.enableDoubletap(true)
.enableAnnotationRendering(true)
.enableAntialiasing(true)
.load()
}

override fun onResume() {
super.onResume()
toolbar?.title = doc?.language
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.getItemId()
if (id == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
}

 

O toolbar está definido como propriedade de classe para que seja possível atualizar o título da atividade no onResume().

Os trechos de código:

...
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
getSupportActionBar()?.setDisplayShowHomeEnabled(true)
...

 

E o método onOptionsItemSelected() estão presentes para seja possível o trabalho com o back button na bar da atividade:

Caso esteja confuso quanto aos métodos utilizados junto a propriedade pdfView, volte a seção Métodos úteis onde explico cada um deles.

Por fim, o que nos resta é a definição desta nova atividade no AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.pdfviwertest">

<application ...>

...
<activity
android:name=".PdfActivity"
android:theme="@style/AppTheme.NoActionBar" />
</application>
</manifest>

Listener de clique na classe adaptadora

Na classe DocAdapter temos de ter o listener de clique implementado para que seja possível enviar um objeto Doc à atividade PdfActivity.

Segue atualização:

class DocAdapter(...) : RecyclerView.Adapter<DocAdapter.ViewHolder>() {
...

inner class ViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView),
View.OnClickListener {
...

override fun onClick(view: View?) {
val intent = Intent(context, PdfActivity::class.java)
intent.putExtra( Doc.DOC_KEY, docList.get( adapterPosition ) )
context.startActivity( intent )
}
}
}

 

A partir daqui o projeto já pode ser testado, mas ainda temos de trabalhar a persistência local da página atual de cada documentação visualizada e também a renderização correta da página de PDF tanto no device em modo portrait quanto em modo landscape.

Renderização correta de PDF em portrait e em landscape

Para que a página atual do PDF esteja cheia em tela tanto no device em portrait quanto em landscape, utilizaremos o método fitToWidth(), de PDFView, junto ao listener de renderização de PDF, OnRenderListener.

Na atividade PdfActivity adicione os seguintes códigos em destaque:

class PdfActivity :
AppCompatActivity(),
OnRenderListener {
...

override fun onCreate(savedInstanceState: Bundle?) {
...

pdfView
.fromAsset( doc?.path )
.defaultPage( 0 )
.scrollHandle( DefaultScrollHandle(this) )
.enableSwipe(true)
.swipeHorizontal(true)
.enableDoubletap(true)
.enableAnnotationRendering(true)
.enableAntialiasing(true)
.onRender(this)
.load()
}
...

override fun onInitiallyRendered(nbPages: Int, pageWidth: Float, pageHeight: Float) {
pdfView.fitToWidth()
}
}

 

O método fitToWidth() tem uma sobrecarga onde é possível passarmos o número da página que deve ser apresentada assim que se inicia a renderização. A versão sem número de página sempre carregará a primeira página disponível, a de índice 0.

Trabalhando a persistência de página atual de documentação

Temos apenas cinco documentações e a persistência local que utilizaremos deverá trabalhar então com cinco chaves de acesso, ou seja, serão poucos dados sendo persistidos.

Para isso utilizaremos o SharedPreferences. Os métodos de trabalho com persistência colocaremos na classe Database, logo, vamos a atualização dela:

class Database {
companion object{
...

fun saveActualPageSP( context: Context, key: String, page: Int ){
context
.getSharedPreferences("PREF", Context.MODE_PRIVATE)
.edit()
.putInt("$key-page", page)
.apply()
}

fun getActualPageSP( context: Context, key: String )
= context
.getSharedPreferences("PREF", Context.MODE_PRIVATE)
.getInt("$key-page", 0)

}
}

 

Para key será utilizada a propriedade path de cada objeto Doc, isso, pois essa propriedade é de valor único e não tem espaços em branco e nem caracteres especiais.

Agora, para facilitar o trabalho de persistência nos códigos de atividade e adapter do projeto, vamos criar métodos públicos na classe Doc para acesso a esses métodos de persistência na classe Database:

class Doc(...) : Parcelable {

fun saveActualPage(context: Context, page: Int ){
Database.saveActualPageSP(context, path, page)
}

fun getActualPage(context: Context ) = Database.getActualPageSP(context, path)

...
}

 

Assim podemos atualizar a PdfActivity para salvar a página atual e também para poder carregar a última página salva. Segue:

class PdfActivity :
AppCompatActivity(),
OnPageChangeListener,
OnRenderListener{
...

override fun onCreate(savedInstanceState: Bundle?) {
...
pdfView
.fromAsset( doc?.path )
.defaultPage( doc?.getActualPage(this) ?: 0 )
.scrollHandle( DefaultScrollHandle(this) )
.enableSwipe(true)
.swipeHorizontal(true)
.enableDoubletap(true)
.enableAnnotationRendering(true)
.enableAntialiasing(true)
.onPageChange(this)
.onRender(this)
.load()
}
...

override fun onPageChanged(page: Int, pageCount: Int) {
doc?.saveActualPage(this, page)
}

override fun onInitiallyRendered(nbPages: Int, pageWidth: Float, pageHeight: Float) {
pdfView.fitToWidth( doc?.getActualPage(this) ?: 0 )
}
}

 

Agora estamos utilizando a sobrecarga com argumento de fitToWidth(), caso contrário o carregamento da página atual sempre seria na primeira página disponível do PDF, a página 0.

Caso ainda não conheça o operador Elvis, ?:, não deixe de depois deste artigo estudar o seguinte: Mantendo dados com a API SavedInstanceState e utilizando o operador "Elvis".

Atualização de última página lida

Para que seja possível a apresentação de última página lida de documentação, teremos primeiro de atualizar o XML de layout de item, o /res/layout/iten_doc.xml, acrescentando um novo TextView:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/tools"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
...

<TextView
android:id="@+id/tv_page_stopped"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_language"
android:layout_alignStart="@+id/tv_language"
android:layout_below="@+id/tv_total_pages"
android:layout_marginTop="6dp" />
</RelativeLayout>

 

Assim, antes de prosseguir para a atualização do código Kotlin de DocAdapter, vamos a uma pequena atualização ao método getActualPage() de Doc:

...
fun getActualPage(context: Context, plusPage: Int = 0 )
= Database.getActualPageSP(context, path) + plusPage
...

 

Essa atualização é necessária para que seja possível a apresentação correta de página nos itens em tela, isso, pois os índices utilizados em código partem de zero, ou seja, caso o usuário tenha visualizado por último a página três, na verdade nós teremos salvo no SharedPreferences o índice dois.

Porém, para que o usuário veja consistência na visualização de última página acessada, ao menos no código de DocAdapter temos de mostrar a numeração correta, por isso o uso do plusPage já iniciando com o valor 0 para não afetar as outras partes do projeto que fazem uso do método getActualPageSP().

Por fim a atualização do ViewHolder de DocAdapter:

class DocAdapter(...) :
RecyclerView.Adapter<DocAdapter.ViewHolder>() {
...

inner class ViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView),
View.OnClickListener {

var ivCover: ImageView
var tvLanguage: TextView
var tvTotalPages: TextView
var tvPageStopped: TextView

init {
itemView.setOnClickListener(this)
ivCover = itemView.findViewById(R.id.iv_cover)
tvLanguage = itemView.findViewById(R.id.tv_language)
tvTotalPages = itemView.findViewById(R.id.tv_total_pages)
tvPageStopped = itemView.findViewById(R.id.tv_page_stopped)
}

fun setData(doc: Doc) {
ivCover.setImageResource( doc.imageRes )
tvLanguage.text = doc.language
tvTotalPages.text = "${doc.pagesNumber} páginas"

tvPageStopped.visibility = if( doc.getActualPage(context) > 0 ){
tvPageStopped.text = "Parou na página ${doc.getActualPage(context, 1)}"
View.VISIBLE
}
else{
View.GONE
}
}
...
}
}

 

No novo código de setData() o que estamos fazendo é trabalhando com o if...else como expressão, onde o TextView tvPageStopped somente será apresentado caso a página atual no SharedPreferences para o item em teste seja maior que a primeira página, página 0.

Quando trabalhando com o if...else no modo "expression", podemos colocar qualquer coisa dentro do bloco de código, porém a última linha de cada deverá ser compátivel, pois é ela que será retornada caso o bloco entre em execução.

Correção na atividade principal

Na MainActivity estamos inicializando o RecyclerView no onCreate(), mas para que seja possível apresentar a última página lida das documentações é perciso carrega o RecyclerView novamente quando o usuário voltar de PdfActivity.

Para isso, na MainActivity, coloque o código de incialização de lista no método onResume() ao invés de no método onCreate():

class MainActivity : AppCompatActivity() {
...

override fun onResume() {
super.onResume()
initRecycler()
}
}

Testes e resultados

Antes de executar o aplicativo, vá ao menu, logo depois em "Build" e então clique em "Rebuild Project". Agora execute o projeto e terá algo como:

Clicando na documentação da linguagem Haskell e indo a até a página 22, temos:

Voltando a atividade principal e depois voltando a documentação de Haskell podemos ver a persistência local funcionando como previsto:

Rotacionando a tela para landscape, temos:

Assim terminamos a apresentação da API PdfViewer por meio de nosso aplicativo de documentações.

Não deixe de se inscrever na lista de emails do Blog, logo ao final do artigo ou ao lado, para que eu possa lhe enviar novos conteúdos, e exclusivos, sobre o dev Android.

Se inscreva também no canal do Blog no YouTube.

Vídeo com implementação passo a passo da biblioteca

A seguir o vídeo com a implementação passo a passo do aplicativo de exemplo com a biblioteca PdfViewer:

Para acesso ao projeto Android sem a implementação da biblioteca em discussão, entre no seguinte GitHub: https://github.com/viniciusthiengo/dot-documentacoes-inicial.

Para acesso ao projeto Android finalizado, já com a implementação da biblioteca PdfViewer, entre no GitHub a seguir: https://github.com/viniciusthiengo/dot-documentacoes-final.

Conclusão

Caso a apresentação de PDF, mesmo que em uma pequena parte de seu projeto, políticas de privacidade, por exemplo, seja algo necessário, há inúmeras APIs gratuitas que permitem que isso seja possível e com poucas linhas de código.

Com a PdfViewer temos um melhor gerenciamento de apresentação de conteúdo. E quando trabalhando com outras APIs, como o SharedPreferences, é possível enriquecer ainda mais a experiência do usuário final.

Para ter acesso a ainda mais APIs de PDF, incluindo APIs de criação deste tipo de arquivo no Android, não deixe de acessar o link a seguir: https://android-arsenal.com/search?q=pdf.

Comente a baixo o que achou ou suas próprias dicas de conteúdos PDF para Android. Não esqueça de se inscrever na lista de emails do Blog, logo ao final do artigo ou ao lado.

Abraço.

Fontes

Documentação PdfViewer API

Sistema de Permissões em Tempo de Execução, Android M

SharedPreferences no Android, Entendendo e Utilizando

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Iniciando com Anko Kotlin. Intenções no AndroidIniciando com Anko Kotlin. Intenções no AndroidAndroid
Colocando Telas de Introdução em Seu Aplicativo AndroidColocando Telas de Introdução em Seu Aplicativo AndroidAndroid
Facilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCodeFacilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCodeAndroid
Segurança e Persistência Android com a Biblioteca HawkSegurança e Persistência Android com a Biblioteca HawkAndroid

Compartilhar

Comentários Facebook (5)

Comentários Blog (3)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
avinicius.adorno (1) (0)
23/07/2017
Os vídeos não abrem no app.
Responder
Vinícius Thiengo (0) (0)
23/07/2017
Vinicius, tudo bem?

Essa é uma limitação da versão atual do aplicativo em algumas APIs do Android, isso devido a atualização da library do YouTube.

A próxima versão terá terá a correção. Abraço.
Responder
Heraldo Gama (1) (0)
20/07/2017
Mais uma excelente postagem, parabéns !!!
Ficou show de bola.
Responder