Android не всегда умел работать с PDF-файлами. Вплоть до версии Android 4.4 KitKat (API 19) у нас не было возможности просматривать PDF-файлы, кроме как используя сторонние приложения, такие как Google Drive PDF Viewer или другой нативный ридер на устройстве.
Начиная с Android 5.0 Lolipop (API 21) появилось API под названием PDFRenderer, с помощью которого можно выводить содержимое PDF-файлов. В этой статье мы создадим приложение, которое открывает PDF-файлы на устройстве для чтения.
Создадим новый проект, указав для него минимальную версию SDK Android 5.0 Lolipop. Для навигации приложения и дальнейшей работы с ним выберем Navigation Drawer Activity.
Шаг 1. Создание списка PDF-файлов
Перед тем, как начать работу, нужно предоставить приложению разрешения. Для этого в AndoridManifest.xml добавим следующие разрешения на чтение и запись.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.androidtools.pdfreader">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
...
На MainActivity мы будем получать список PDF-файлов, которые находятся в хранилище устройства. Для этого на файле разметки content_main.xml добавим компонент ListView.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_margin="10dp"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="ru.androidtools.pdfreader.MainActivity"
>
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
/>
</LinearLayout>
Также нужно создать разметку для элемента в списке. Создадим в res/layout файл разметки list_item.xml и добавим в него следующий код.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/txtFileName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:textColor="#000000"
android:textSize="16sp"
/>
</LinearLayout>
Найденные файлы нужно сохранять в модель, чтобы затем передать в список. Для этого создадим класс PdfFile, в котором будут определены 2 поля (имя файла и путь), а также сеттеры и геттеры для изменения полей.
public class PdfFile {
private String fileName;
private String filePath;
PdfFile(String fileName, String filePath) {
this.fileName = fileName;
this.filePath = filePath;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
}
После этого в коде активности перед началом работы нужно проверить версию Android. Если версия ниже Android 6.0 M – инициализируем создание списка, в противном случае нужно проверить разрешения.
private ListView listView;
private static final int REQUEST_PERMISSION = 1;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
listView = findViewById(R.id.listView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkPermission();
} else {
initViews();
}
}
Метод checkPermission() проверяет наличие разрешения на чтение внешнего хранилища. Если разрешения имеются, то инициализируем создание списка, иначе запрашиваем разрешения. Пользователю будет показан диалог, предлагающий предоставить приложению разрешение, результат этого вернётся в метод onRequestPermissionsResult() активности.
private static final int REQUEST_PERMISSION = 1;
...
private void checkPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
initViews();
} else {
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE
}, REQUEST_PERMISSION);
}
}
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initViews();
} else {
// в разрешении отказано (в первый раз, когда чекбокс "Больше не спрашивать" ещё не показывается)
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_EXTERNAL_STORAGE)) {
finish();
}
// в разрешении отказано (выбрано "Больше не спрашивать")
else {
// показываем диалог, сообщающий о важности разрешения
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(
"Вы отказались предоставлять разрешение на чтение хранилища.\n\nЭто необходимо для работы приложения."
+ "\n\n"
+ "Нажмите \"Предоставить\", чтобы предоставить приложению разрешения.")
// при согласии откроется окно настроек, в котором пользователю нужно будет вручную предоставить разрешения
.setPositiveButton("Предоставить", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
finish();
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", getPackageName(), null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
})
// закрываем приложение
.setNegativeButton("Отказаться", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
finish();
}
});
builder.setCancelable(false);
builder.create().show();
}
}
break;
}
}
}
В методе initViews() нам нужно инициализировать список. Для этого вызываем внутри метод initList(), который будет рекурсивно вызываться до тех пор, пока не будет проверен каждый каталог. В результате выполнения initList() будет заполнен список найденными PDF-файлами, который затем будет передан в адаптер. В адаптере переопределяются методы в соответствии с тем, что нам нужно, в частности в методе getView() мы получаем созданную для элемента разметку и устанавливаем в поле имя файла.
private ArrayList list = new ArrayList<>();
...
private BaseAdapter adapter = new BaseAdapter() {
@Override public int getCount() {
return list.size();
}
@Override public PdfFile getItem(int i) {
return list.get(i);
}
@Override public long getItemId(int i) {
return i;
}
@Override public View getView(int i, View view, ViewGroup viewGroup) {
View v = view;
if (v == null) {
v = getLayoutInflater().inflate(R.layout.list_item, viewGroup, false);
}
PdfFile pdfFile = getItem(i);
TextView name = v.findViewById(R.id.txtFileName);
name.setText(pdfFile.getFileName());
return v;
}
};
private void initViews() {
// получаем путь до внешнего хранилища
String path = Environment.getExternalStorageDirectory().getAbsolutePath();
initList(path);
// устанавливаем адаптер в ListView
listView.setAdapter(adapter);
// когда пользователь выбирает PDF-файл из списка, открываем активность для просмотра
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
Intent intent = new Intent(MainActivity.this, PdfActivity.class);
intent.putExtra("keyName", list.get(i).getFileName());
intent.putExtra("fileName", list.get(i).getFilePath());
startActivity(intent);
}
});
}
private void initList(String path) {
try {
File file = new File(path);
File[] fileList = file.listFiles();
String fileName;
for (File f : fileList) {
if (f.isDirectory()) {
initList(f.getAbsolutePath());
} else {
fileName = f.getName();
if (fileName.endsWith(".pdf")) {
list.add(new PdfFile(fileName, f.getAbsolutePath()));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
Шаг 2. Создание активности для просмотра
Здесь мы создадим активность, в которой будет просматриваться выбранный PDF-файл. Отрендеренная страница PDF будет загружаться в компонент ImageView. Также здесь будут располагаться кнопки переключения страниц и увеличения/уменьшения масштаба страницы.
Добавим Empty Activity, назвав PdfActivity, и в коде разметки активности добавим следующий код.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".PdfActivity"
>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
>
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
>
<ImageView
android:id="@+id/imgView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:contentDescription="@null"
/>
</HorizontalScrollView>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/btnPrevious"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:background="@null"
android:text="Назад"
android:textColor="@drawable/btn_color"
style="?android:attr/buttonBarButtonStyle"
/>
<Button
android:id="@+id/btnNext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:background="@android:color/white"
android:text="Вперёд"
android:textColor="@drawable/btn_color"
style="?android:attr/buttonBarButtonStyle"
/>
<ImageButton
android:id="@+id/zoomout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="0dp"
android:layout_weight="0.5"
android:background="@null"
android:padding="8dp"
android:src="@drawable/ic_zoom_out_black_24dp"
/>
<ImageButton
android:id="@+id/zoomin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="0dp"
android:layout_weight="0.5"
android:background="@null"
android:padding="8dp"
android:src="@drawable/ic_zoom_in_black_24dp"
/>
</LinearLayout>
</LinearLayout>
При запуске активности в методе onCreate() получаем имя и путь файла из интента, а также определяем компоненты и устанавливает слушатели на кнопки.
private String path;
private ImageView imgView;
private Button btnPrevious, btnNext;
private int currentPage = 0;
private ImageButton btn_zoomin, btn_zoomout;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pdf);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
path = getIntent().getStringExtra("fileName");
setTitle(getIntent().getStringExtra("keyName"));
// если в банлде есть номер страницы - забираем его
if (savedInstanceState != null) {
currentPage = savedInstanceState.getInt(CURRENT_PAGE, 0);
}
imgView = findViewById(R.id.imgView);
btnPrevious = findViewById(R.id.btnPrevious);
btnNext = findViewById(R.id.btnNext);
btn_zoomin = findViewById(R.id.zoomin);
btn_zoomout = findViewById(R.id.zoomout);
// устанавливаем слушатели на кнопки
btnPrevious.setOnClickListener(this);
btnNext.setOnClickListener(this);
btn_zoomin.setOnClickListener(this);
btn_zoomout.setOnClickListener(this);
}
Затем в методе onStart() вызывается метод openPdfRenderer(), в котором происходит получение дескриптора файла для поиска PDF и создание объекта PdfRenderer. После этого вызывается метод displayPage(), который рендерит выбранную страницу.
private PdfRenderer pdfRenderer;
private PdfRenderer.Page curPage;
private ParcelFileDescriptor descriptor;
private float currentZoomLevel = 5;
@Override public void onStart() {
super.onStart();
try {
openPdfRenderer();
displayPage(currentPage);
} catch (Exception e) {
Toast.makeText(this, "PDF-файл защищен паролем.", Toast.LENGTH_SHORT).show();
}
}
private void openPdfRenderer() {
File file = new File(path);
descriptor = null;
pdfRenderer = null;
try {
descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
pdfRenderer = new PdfRenderer(descriptor);
} catch (Exception e) {
Toast.makeText(this, "Ошибка", Toast.LENGTH_LONG).show();
}
}
private void displayPage(int index) {
if (pdfRenderer.getPageCount() <= index) return;
// закрываем текущую страницу
if (curPage != null) curPage.close();
// открываем нужную страницу
curPage = pdfRenderer.openPage(index);
// определяем размеры Bitmap
int newWidth = (int) (getResources().getDisplayMetrics().widthPixels * curPage.getWidth() / 72
* currentZoomLevel / 40);
int newHeight =
(int) (getResources().getDisplayMetrics().heightPixels * curPage.getHeight() / 72
* currentZoomLevel / 64);
Bitmap bitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
Matrix matrix = new Matrix();
float dpiAdjustedZoomLevel = currentZoomLevel * DisplayMetrics.DENSITY_MEDIUM
/ getResources().getDisplayMetrics().densityDpi;
matrix.setScale(dpiAdjustedZoomLevel, dpiAdjustedZoomLevel);
curPage.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
// отображаем результат рендера
imgView.setImageBitmap(bitmap);
// проверяем, нужно ли делать кнопки недоступными
int pageCount = pdfRenderer.getPageCount();
btnPrevious.setEnabled(0 != index);
btnNext.setEnabled(index + 1 < pageCount);
btn_zoomout.setEnabled(currentZoomLevel != 2);
btn_zoomin.setEnabled(currentZoomLevel != 12);
}
Что касается метода displayPage(), в нём для каждой страницы, которую нужно отрендерить, открываем эту страницу, рендерим её и закрываем страницу. В результате рендера страница сохраняется в Bitmap с заданными размерами и зумом, после чего загружается в ImageView.
Обработчики кнопок вынесены в onClick(), здесь ничего особенного нет: кнопки “Назад” и “Вперёд” изменяют текущий индекс страницы в нужную сторону и вызывают метод displayPage(), аналогично кнопки зума изменяют уровень зума и вызывают метод displayPage() с текущим индексом.
@Override public void onClick(View v) {
switch (v.getId()) {
case R.id.btnPrevious: {
// получаем индекс предыдущей страницы
int index = curPage.getIndex() - 1;
displayPage(index);
break;
}
case R.id.btnNext: {
// получаем индекс следующей страницы
int index = curPage.getIndex() + 1;
displayPage(index);
break;
}
case R.id.zoomout: {
// уменьшаем зум
--currentZoomLevel;
displayPage(curPage.getIndex());
break;
}
case R.id.zoomin: {
// увеличиваем зум
++currentZoomLevel;
displayPage(curPage.getIndex());
break;
}
}
}
Для того, чтобы при перевороте экрана запомнить текущий индекс страницы, переопределён метод onSaveInstanceBundle(), в котором в бандл помещается индекс страницы. Затем этот индекс забирается в onCreate() активности, что было показано выше.
@Override protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (curPage != null) {
outState.putInt(CURRENT_PAGE, curPage.getIndex());
}
}
При завершении работы и закрытии активности нужно закрыть PdfRenderer, для этого в переопределённом методе onStop() вызывается метод closePdfRenderer(), в котором удаляются все данные рендера.
@Override public void onStop() {
try {
closePdfRenderer();
} catch (IOException e) {
e.printStackTrace();
}
super.onStop();
}
private void closePdfRenderer() throws IOException {
if (curPage != null) curPage.close();
if (pdfRenderer != null) pdfRenderer.close();
if (descriptor != null) descriptor.close();
}
В результате получилось простое приложение, способное открывать и показывать PDF-файлы.
Заключение
В этой статье мы рассмотрели, как можно использовать PdfRenderer API для отображения PDF-файлов в приложении. В следующей статье мы рассмотрим, как создать свой PDF-файл.






Доброго времени суток.
Всё круто, но)
Для рендеринга нужно что бы страница умещалась во вьюху
// определяем размеры Bitmap
int newWidth = (int) (getResources().getDisplayMetrics().widthPixels * curPage.getWidth() / 72
* currentZoomLevel / 40);//45
int newHeight = (int) (getResources().getDisplayMetrics().heightPixels * curPage.getHeight() / 72
* currentZoomLevel / 65);//90
Bitmap bitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
Matrix matrix = new Matrix();
float dpiAdjustedZoomLevel = currentZoomLevel * DisplayMetrics.DENSITY_MEDIUM
/ getResources().getDisplayMetrics().densityDpi;
matrix.setScale(dpiAdjustedZoomLevel, dpiAdjustedZoomLevel);
curPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
Далее, вместо скролов и ImageView, я сделал так:
Отлично маштабируется и намного приятнее)
Но всё равно спасибо, статья неплохая.
com.github.chrisbanes.photoview.PhotoView
Не вставился код xml
кнопки зума я убрал