PDF no Android

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

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

PDF no Android

Vinícius Thiengo
(12434) (18)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição1ª
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

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 (ouvidores de eventos);
  • 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, por exemplo, para poder prosseguir com os estudos:

Aplicativo Android Dot.Documentações para PDFs

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.

Antes de prosseguir, não esqueça de se inscrever ðŸ“«na lista de e-mails do Blog para receber, em primeira mão, todos os conteúdos de desenvolvimento Android exclusivos aqui do Blog.

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 (em inglês), 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:

Tela principal do aplicativo Dot.Documentações

E a seguinte estrutra de projeto:

Estrutura do projeto no Android Studio IDE

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

Diagrama do layout iten_doc.xml

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:

Visualização do projeto, no Android Studio IDE, em Project

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

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

Criando o folder /assets

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:

Colocando arquivos PDF no folder /assets

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:

Diagrama do layout activity_pdf.xml

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

 

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:

Barra de topo do aplicativo Dot.Documentações

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 compatível, 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 é preciso carrega o RecyclerView novamente quando o usuário voltar de PdfActivity.

Para isso, na MainActivity, coloque o código de inicializaçã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:

Tela inicial do app Dot.Documentações

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

PDF da linguagem Haskell no aplicativo Dot.Documentações

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

Última página acessada no PDF da linguagem Haskell no aplicativo Dot.Documentações

Rotacionando a tela para landscape, temos:

Aplicativo Dot.Documentações na horizontal, landscape

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:

Comente a baixo o que achou ou suas próprias dicas de conteúdos PDF para Android.

E... não esqueça de se inscrever na lista de e-mails ðŸ“© do Blog, logo ao final do artigo ou ao lado.

Abraço.

Fonte

Documentação PdfViewer API

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
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

Comentários Blog (18)

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...
Thyago Neves Silvestre (1) (0)
03/06/2021
Muito bom este artigo como sempre. Usei em um aplicativo que publiquei 31 de Janeiro e já está chegando nos 1000 downloads, me baseei neste artigo para implementar.

https://play.google.com/store/apps/details?id=com.t7droid.nrs_normas_regulamentadoras
Responder
Vinícius Thiengo (0) (0)
05/06/2021
Thyago, tudo bem?

Excelente o match do artigo com o seu aplicativo.

É isso.

Abraço.
Responder
Joao Hemerson (0) (0)
08/12/2019
Gostaria de sua ajuda, criei um app usando iText para gerar um relatório, porem o código pra abrir o relatório quando só funciona nas versões 6.0 e anteriores, para as novas versões do android o código não funciona, o relatório é criado normalmente, mas não abre o pdf automaticamente.

Segue o código...

private void previewPdf() {

        Uri uri = Uri.fromFile(pdfFile);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(uri, "application/pdf");
        try {
            context.startActivity(intent);
        }catch (ActivityNotFoundException e){
            Toast.makeText(context, "App para abrir o arquivo não encontrado", Toast.LENGTH_SHORT).show();
        }

    }
Responder
Vinícius Thiengo (0) (0)
30/12/2019
João, tudo bem?

Neste caso preciso que você me informe quais as mensagens de erro que estão sendo apresentadas assim que o problema ocorre nas versões acima do Android 6.

Como obter essas mensagens de erro?

Nos logs do Android Studio IDE.

Caso você não saiba como fazer isso, peço que primeiro estude todo o conteúdo do link a seguir: https://developer.android.com/studio/debug/am-logcat?hl=pt-br

O conteúdo do link acima é simples, de muita importância para qualquer nível de desenvolvedor Android e está todo em português.

João, é isso.

Com a pilha de erros "em mãos" é possível lhe dar ao menos um "norte" para solucionar o problema.

Surgindo mais dúvidas, pode perguntar.

Bons estudos.

Abraço.
Responder
Maxuel Saldanha (1) (0)
14/06/2019
Gostaria de sua ajuda quanto a PDF no Android, após uma atualização do Android, meu recyclerview, toda vez que chamo o evento do onClick, ao clicar no botão de retornar, ele acrescenta um espaço abaixo do item, já tem de tudo mas o problema persiste, teria ideia do que está acontecendo?
Responder
Vinícius Thiengo (0) (0)
16/08/2019
Maxuel, tudo bem?

Confesso que esse problema do espaço em branco é muito estranho se todo o algoritmo do adapter do RecyclerView não estiver, em algum momento, adicionando um novo componente na lista.

Se possível, realize testes com AVDs, ou aparelhos reais, que estejam com a mesma versão Android testada no device que apresentou o problema e também com outras versões.

Se os seus testes tiverem ocorrido em um emulador AVD, então há sim grandes chances de isso ser um bug nele. Acredite, esses emuladores, mesmo que com pouca frequência, apresentam falhas que não representam problemas no aplicativo.

Abraço.
Responder
Eduardo (1) (0)
24/09/2018
Thiengo, essa biblioteca é muito boa mesmo, eu tentei utilizá-la em meu app mas tive que removê-la por causa de um ponto negativo dela: a apk final subiu de 9 pra 22 mb, e como eu só precisava exibir o conteúdo mudei pro PdfRenderer nativo do Android
Responder
Vinícius Thiengo (1) (0)
24/09/2018
Eduardo, obrigado pela contribuição.

Felizmente o Android não tem mais o pequeno limite de 50MB por APK.

Abraço.
Responder
03/07/2018
Thiengo, clonei esse seu excelente projeto do GitHub mas estou com dificuldade para criar novas sub pastas no drawable. Já inclui sourceSets no Gradle e o plugin android file group na minha aplicação. Vc poderia me dar uma pista de como resolver essa questão? Grato
Responder
Vinícius Thiengo (0) (0)
08/07/2018
Carlos, tudo bem?

Não é possível criar sub-diretórios dentro de qualquer /drawable. Essa é uma das regras de negócios do dev Android.

Você consegue fazer isso, criar sub-folders, no /assets, como utilizado no conteúdo do projeto deste artigo.

É isso de que precisa, sub-diretórios no /assets, certo?

Abraço.
Responder
08/07/2018
Ok! Obrigado Thiengo, já consegui criar seguindo a sua orientação.Abs
Responder
jonathan (1) (0)
29/01/2018
Olá Thiengo, é free para fins empresariais?
Responder
Vinícius Thiengo (0) (0)
29/01/2018
Jonathan, tudo bem?

Sim, é livre também para fins comerciais, pois a licença é a Apache V2: https://pt.wikipedia.org/wiki/Licen%C3%A7a_Apache#Licen%C3%A7a_Apache_2.0

Somente tem de referenciar a fonte corretamente.

Abraço.
Responder
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)
19/07/2017
Mais uma excelente postagem, parabéns !!!
Ficou show de bola.
Responder
28/01/2018
Muito muito bom . Alguém sabe como posso adquirir o curso dele ,de protótipos?
Responder
Vinícius Thiengo (0) (0)
29/01/2018
Tiago, tudo bem?

Entre em contato pelo email thiengocalopsita@gmail.com que lhe envio um cupom promocional.

Abraço.
Responder