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
кнопки зума я убрал