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 добавим следующие разрешения на чтение и запись.
На MainActivity мы будем получать список PDF-файлов, которые находятся в хранилище устройства. Для этого на файле разметки content_main.xml добавим компонент ListView.
Найденные файлы нужно сохранять в модель, чтобы затем передать в список. Для этого создадим класс 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 – инициализируем создание списка, в противном случае нужно проверить разрешения.
Метод 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, и в коде разметки активности добавим следующий код.
При запуске активности в методе 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(), который рендерит выбранную страницу.
Что касается метода 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() активности, что было показано выше.
При завершении работы и закрытии активности нужно закрыть 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-файл.
Работа с PDF. Чтение PDF-файлов: 2 комментария
Ратмир
Доброго времени суток.
Всё круто, но)
Для рендеринга нужно что бы страница умещалась во вьюху
// определяем размеры Bitmap
int newWidth = (int) (getResources().getDisplayMetrics().widthPixels * curPage.getWidth() / 72
* currentZoomLevel / 40);//45
Доброго времени суток.
Всё круто, но)
Для рендеринга нужно что бы страница умещалась во вьюху
// определяем размеры 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
кнопки зума я убрал