Пишем читалку электронных книг на Android с помощью FBReader SDK

В настоящее время наиболее популярными и удобными способами чтения являются электронные книги. Их можно взять с собой куда угодно, они экономят бумагу, отображение текста в них можно настраивать так, как удобно читателю. По этим причинам появилось множество форматов электронных книг, основные из которых это EPUB, FB2 и MOBI. Всем известные DOC и TXT тоже являются форматами электронных книг, однако их возможности по сравнению с вышеуказанными весьма ограничены. У каждого из форматов есть свои преимущества, однако наиболее часто используемыми являются EPUB и FB2, причём второй популярен только у нас в России.

Одной из особенностей этих форматов является то, что для них нужна специальная программа, которая может работать с конкретным форматом. Поэтому в App Store или Google Play можно найти большое количество разнообразных читалок под все известные форматы. В этой статье мы попробуем написать свою собственную читалку электронных книг. Для этой цели мы воспользуемся специальной SDK от разработчиков FBReader, который доступен по следующей ссылке.

В сентябре этого года FBReader выпустили свой собственный SDK для Android, позволяющий создавать свои собственные читалки на его основе. FBReader SDK обладает большим функционалом, который включает в себя следующие возможности:

  • Открытие любых файлов электронных книг (ePub, fb2, mobi, и так далее)
  • Чтение метаданных из файла книги
  • Особенности движка FBReader
  • Возможность менять стиль текста книги

Кроме того, библиотека предоставляет и различные дополнительные возможности для реализации в приложениях, такие как:

  • Выделение текста
  • Закладки
  • Поиск по тексту
  • Оглавление
  • Дополнительная навигация по тексту
  • Поддержка сносок и гиперссылок
  • Настройка яркости

Отдельно стоит отметить, что, хоть FBReader SDK и можно скачать с сайта бесплатно, после встраивания в приложение она будет работать в демо-режиме, который имеет полный функционал, но показывает только несколько первых страниц книги. Для того, чтобы получить полную версию библиотеки, нужно купить её на сайте разработчика и, следуя инструкциям, получить специальный ключ SDK, который затем добавляется в приложение.

В этой статье мы рассмотрим, как на основе FBReader SDK создать свою собственную читалку электронных книг и посмотрим её возможности.

Начнём с добавления SDK в свой проект. Для этого создадим новый проект с пустой активностью. В качестве минимального API укажем API 16.

В файле build.gradle модуля проекта добавим в блок allprojects ссылку для скачивания нужных нам библиотек.

allprojects {
    repositories {
        ...
        maven {
            url "https://sdk.fbreader.org/maven"
        }
    }
}

Затем в файле build.gradle модуля приложения добавим зависимости с библиотеками в блок dependencies.

dependencies {
    ...
    implementation 'com.googlecode.json-simple:json-simple:1.1'
    implementation 'org.fbreader:book:1.0.0-rc01'
    implementation 'org.fbreader:config_client:1.0.0-rc01'
    implementation 'org.fbreader:config_provider:1.0.0-rc01'
    implementation 'org.fbreader:filesystem:1.0.0-rc01'
    implementation 'org.fbreader:fontentry:1.0.0-rc01'
    implementation 'org.fbreader:format_interface:1.0.0-rc01'
    implementation 'org.fbreader:image:1.0.0-rc01'
    implementation 'org.fbreader:intent:1.0.0-rc01'
    implementation 'org.fbreader:language:1.0.0-rc01'
    implementation 'org.fbreader:loader:1.0.0-rc01'
    implementation 'org.fbreader:options:1.0.0-rc01'
    implementation 'org.fbreader:text_client:1.0.0-rc01'
    implementation 'org.fbreader:text_provider:1.0.0-rc01'
    implementation 'org.fbreader:text_util:1.0.0-rc01'
    implementation 'org.fbreader:text_view:1.0.0-rc01'
    implementation 'org.fbreader:toc:1.0.0-rc01'
    implementation 'org.fbreader:util:1.0.0-rc01'
    implementation 'org.fbreader:view:1.0.0-rc01'
    implementation 'org.fbreader:vimgadgets:1.0.0-rc01'
    implementation 'org.fbreader:zip-amse:1.0.0-rc01'
}

Как вы можете заметить, здесь также присутствует библиотека «com.googlecode.json-simple:json-simple:1.1». Она пригодится чуть позже для работы с JSON-файлами.

Библиотек, входящих в FBReader SDK довольно много, однако они разделены так, чтобы можно было подключать только те, которые нужны для конкретного случая.

В этом же файле в блок android добавим следующие строки, которые должны обеспечить успешную сборку проекта.

android {
  ...

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

На сайте SDK разработчики также предоставляют исходный код различных компонентов, которые упрощают реализацию интерфейса. Скачаем его и добавим в проект. Для этого перенесём оттуда папку fbreader в папку своего проекта. После этого в файле settings.gradle пропишем эту папку и пути до нужных модулей.

include 'fbreader_extras'
project(':fbreader_extras').projectDir = new File('fbreader/extras')
include 'fbreader_styles'
project(':fbreader_styles').projectDir = new File('fbreader/styles')
include 'fbreader_text_extras'
project(':fbreader_text_extras').projectDir = new File('fbreader/text_extras')

Теперь добавим зависимости с ними в файле build.gradle модуля приложения.

dependencies {
  ...
  implementation project(':fbreader_extras')
  implementation project(':fbreader_styles')
  implementation project(':fbreader_text_extras')
}

Когда IDE пересоберёт проект, мы увидим добавленные модули в списке слева.

На этом подключение SDK завершено и можно приступать к непосредственно созданию приложения. В конце статьи у нас должна получиться простая читалка с двумя активностями, одна из которых будет отображать список электронных книг на устройстве, а вторая открывать выбранный файл.

Начнём с разметки. Для MainActivity добавим на экран компонент RecyclerView, который будет содержать собственно список файлов. Для этого добавим в файл layout/activity_main.xml следующий код.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    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"
    tools:context=".MainActivity"
    android:paddingRight="12dp"
    android:paddingLeft="12dp"
    >

  <androidx.recyclerview.widget.RecyclerView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:layout_marginTop="6dp"
      android:id="@+id/rv_files"
      />

</RelativeLayout>

Теперь инициализируем этот объект в коде активности. Откроем файл MainActivity.java и добавим следующий код.

public class MainActivity extends AppCompatActivity {
  private RecyclerView rvFiles;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    rvFiles = findViewById(R.id.rv_files);
    rvFiles.setLayoutManager(new LinearLayoutManager(this));
  }
}

Для того, чтобы работать с файлами, хранящимися на устройстве, нам потребуется запрашивать специальное разрешение. Добавим в манифест приложения AndroidManifest.xml следующие разрешения.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ru.androidtools.multireader">

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

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

Начиная с API 23 это разрешение необходимо запрашивать у пользователя в рантайме. Для этого при запуске приложения будем проверять версию Android на устройстве и при необходимости отправлять пользователю запрос на получение разрешения.

public class MainActivity extends AppCompatActivity {
  ...
  private static final int REQUEST_PERMISSION = 101;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    rvFiles = findViewById(R.id.rv_files);
    rvFiles.setLayoutManager(new LinearLayoutManager(this));

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      checkPermission();
    } else {
      generateList();
    }
  }

  @TargetApi(Build.VERSION_CODES.M) private void checkPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
        == PackageManager.PERMISSION_GRANTED) {
      generateList();
    } else {
      ActivityCompat.requestPermissions(this,
          new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, REQUEST_PERMISSION);
    }
  }
}

Получить результат запроса можно, переопределив метод активности onRequestPermissionResult(). Если разрешение приложению было предоставлено, продолжаем дальнейшую работу.

@Override 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) {
        generateList();
      }
      break;
    }
  }
}

В методе generateList() мы будем искать нужные файлы на устройстве и отображать их в списке приложения. Для этого создадим AsyncTask, который будет выполнять всю работу в отдельном потоке и возвращать готовый список с найденными файлами. Добавим следующий код в MainActivity.

private void generateList() {
  List<StorageBean> storageBeans = StorageUtils.getStorageData(this); // получение списка смонтированных карт памяти на устройстве
  List<String> paths = new ArrayList<>();
  if (storageBeans != null) {
    for (StorageBean storageBean : storageBeans) {
      paths.add(storageBean.getPath());
    }
  } else {
    String path = Environment.getExternalStorageDirectory().getAbsolutePath();
    paths.add(path);
  }
  ListFilesTask listFilesTask = new ListFilesTask(paths);
  listFilesTask.setListener(new ListFilesTask.ListFilesListener() {
    @Override public void onTaskCompleted(List<File> files) {
    }
  });
  listFilesTask.execute();
}

static class ListFilesTask extends AsyncTask<Void, Void, List<File>> {
  public interface ListFilesListener {
    void onTaskCompleted(List<File> files);
  }

  private ListFilesListener listener;
  private List<String> startPaths;
  private List<File> files;
  private boolean completed;

  public ListFilesTask(List<String> startPaths) {
    this.startPaths = new ArrayList<>(startPaths);
    this.files = new ArrayList<>();
    this.completed = false;
  }

  public void setListener(ListFilesListener listener) {
    this.listener = listener;
    if (completed && listener != null && files != null) {
      listener.onTaskCompleted(files);
    }
  }

  @Override protected List<File> doInBackground(Void... voids) {
    List<File> fileList = new ArrayList<>();
    for (String s : startPaths) {
      searchFiles(fileList, new File(s));
    }
    return fileList;
  }

  @Override protected void onPostExecute(List<File> files) {
    completed = true;
    if (listener != null) {
      listener.onTaskCompleted(files);
    } else {
      this.files = new ArrayList<>(files);
    }
  }

  private void searchFiles(List<File> list, File dir) {
    String epubPattern = ".epub";
    String fb2Pattern = ".fb2";

    File[] listFiles = dir.listFiles();
    if (listFiles != null) {
      for (File listFile : listFiles) {
        if (listFile.isDirectory()) {
          searchFiles(list, listFile);
        } else {
          if (listFile.getName().endsWith(epubPattern) || listFile.getName()
              .endsWith(fb2Pattern)) {
            list.add(listFile);
          }
        }
      }
    }
  }
}

Полученный результат нужно отобразить. Для этого нам понадобится адаптер для RecyclerView и модель, в которой будут храниться данные элементов списка. Создадим простой класс BookFile, в котором мы будем хранить имя файла и путь до него на устройстве.

public class BookFile {
  private String filename, path;

  public BookFile(String filename, String path) {
    this.filename = filename;
    this.path = path;
  }

  public String getFilename() {
    return filename;
  }

  public String getPath() {
    return path;
  }

  @Override public int hashCode() {
    return filename.hashCode() + path.hashCode();
  }

  @Override public boolean equals(Object obj) {
    if (obj instanceof BookFile) {
      BookFile bookFile = (BookFile) obj;
      return this.filename.equals(bookFile.getFilename()) && this.path.equals(bookFile.getPath());
    }
    return false;
  }
}

Теперь нам нужен адаптер, в который мы будем загружать список. Добавим разметку для элемента списка, для этого создадим файл layout/file_list_item.xml и добавим в него следующий код.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="2dp"
    android:layout_marginTop="2dp"
    android:orientation="vertical"
    android:paddingBottom="6dp"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:paddingTop="6dp"
    >

  <TextView
      android:id="@+id/tv_filename"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="filename"
      android:textSize="16sp"
      />

  <TextView
      android:id="@+id/tv_path"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginTop="2dp"
      android:text="path"
      android:textSize="14sp"
      />

</LinearLayout>

Теперь создадим класс BooksAdapter, который будет наследовать от RecyclerView.Adapter<>. В нём нам нужно инициализировать элементы списка, загружая в них пути и имена файлов электронных книг. Таким образом, код адаптера будет выглядеть следующим образом.

public class BooksAdapter extends RecyclerView.Adapter<BooksAdapter.BookHolder> {
  public interface BookListener {
    void onBookOpen(BookFile bookFile);
  }

  private BookListener listener;
  private List<BookFile> books;

  public BooksAdapter(List<BookFile> books, BookListener listener) {
    this.books = new ArrayList<>(books);
    this.listener = listener;
  }

  @NonNull @Override public BookHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View view =
        LayoutInflater.from(parent.getContext()).inflate(R.layout.file_list_item, parent, false);
    return new BookHolder(view);
  }

  @Override public void onBindViewHolder(@NonNull BookHolder holder, int position) {
    BookFile bookFile = books.get(position);
    holder.bind(bookFile);
  }

  @Override public int getItemCount() {
    return books.size();
  }

  class BookHolder extends RecyclerView.ViewHolder {
    private LinearLayout itemLayout;
    private TextView tvPath, tvName;

    public BookHolder(@NonNull View itemView) {
      super(itemView);
      itemLayout = itemView.findViewById(R.id.item_layout);
      tvName = itemView.findViewById(R.id.tv_filename);
      tvPath = itemView.findViewById(R.id.tv_path);
    }

    public void bind(BookFile bookFile) {
      tvPath.setText(bookFile.getPath());
      tvName.setText(bookFile.getFilename());
      itemLayout.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View view) {
          listener.onBookOpen(bookFile);
        }
      });
    }
  }
}

Кроме того, между активностью и адаптером пробрасывается интерфейс, с помощью которого мы будем передавать в активность файл, который пользователь хочет открыть.

Вернёмся в главную активность. В методе generateList() мы запускаем AsyncTask и привязываем к нему слушатель, который должен вызывать метод onTaskCompleted() при завершении работы. В этом методе нам нужно составить список, отсортировать его по имени и передать в адаптер. Добавим сюда следующий код:

listFilesTask.setListener(new ListFilesTask.ListFilesListener() {
  @Override public void onTaskCompleted(List<File> files) {
    if (!isFinishing()) {
      List<BookFile> bookFiles = new ArrayList<>();
      for (File f : files) {
        BookFile bookFile = new BookFile(f.getName(), f.getAbsolutePath());
        if (!bookFiles.contains(bookFile)) bookFiles.add(bookFile);
      }
      Collections.sort(bookFiles, new Comparator<BookFile>() {
        @Override public int compare(BookFile bookFile, BookFile t1) {
          return bookFile.getFilename().compareToIgnoreCase(t1.getFilename());
        }
      });
      rvFiles.setAdapter(new BooksAdapter(bookFiles, new BooksAdapter.BookListener() {
        @Override public void onBookOpen(BookFile bookFile) {

        }
      }));
    }
  }
});

Теперь, запустив приложение, мы можем увидеть список файлов электронных книг, которые есть на устройстве.

 

Однако нам нужно этот файл открыть и прочитать, в этом нам и поможет FBReader SDK. Создадим новую активность, назовём её ReaderActivity. Теперь из скачанного исходного кода нам понадобится несколько классов. Откроем в папке с исходным кодом samples/extensions/src/main и скопируем себе в проект целиком папку res, за исключением res/layout/main.xml, и из java/org/fbreader/sample/extensions скопируем все классы, кроме MainActivity.java. Эти классы содержат готовые компоненты, которые помогут нам в создании читалки, при желании их можно модифицировать в соответствии с потребностями.

Теперь откроем layout/reader_activity.xml и добавим в него следующий код разметки.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/widget_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

  <ru.androidtools.multireader.TextWidgetExt
      android:id="@+id/text_widget"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:focusable="true"
      android:scrollbars="vertical"
      android:scrollbarAlwaysDrawVerticalTrack="true"
      android:fadeScrollbars="false"
      />

  <TextView
      android:id="@+id/error_message"
      android:textAppearance="?android:attr/textAppearanceLarge"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerInParent="true"
      android:text="The app couldn't open the book"
      android:visibility="gone"
      />

</RelativeLayout>

TextWidgetExt является обёрткой класса библиотеки TextWidget, этот класс является основным и именно он отображает содержимое книги.

Инициализируем теперь эти объекты в коде активности ReaderActivity.java.

public class ReaderActivity extends AppCompatActivity {
  private TextWidgetExt widget;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_reader);

    widget = findViewById(R.id.text_widget);
    View errorView = findViewById(R.id.error_message);
    widget.setVisibility(View.VISIBLE);
    errorView.setVisibility(View.GONE);
  }
}

Мы должны получить от главной активности путь до файла, который пользователь хочет открыть. Для этого мы будем передавать интент из одной активности в другую, содержащий нужный путь. Вернёмся в MainActivity.java и добавим код создания интента в метод onBookOpen() интерфейса адаптера.

rvFiles.setAdapter(new BooksAdapter(bookFiles, new BooksAdapter.BookListener() {
  @Override public void onBookOpen(BookFile bookFile) {
    Intent intent = new Intent(MainActivity.this, ReaderActivity.class);
    intent.putExtra(ReaderActivity.EXTRA_PATH, bookFile.getPath());
    startActivity(intent);
  }
}));

После этого отправленный интент нам нужен получить в ReaderActivity.java при старте активности. Добавим в onCreate() запись пути файла.

...
public final static String EXTRA_PATH = "EXTRA_PATH";
private String filepath;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_reader);

  widget = findViewById(R.id.text_widget);
  View errorView = findViewById(R.id.error_message);
  widget.setVisibility(View.VISIBLE);
  errorView.setVisibility(View.GONE);

  filepath = getIntent().getStringExtra(EXTRA_PATH);
  }
}

Полученный путь мы передаём в метод setBook() у объекта TextWidgetExt. После этого он должен нам вернуть объект Book, который можно получить с помощью метода TextWidgetExt controller(). Если объект не будет равен null, значит загрузка содержимого прошла успешно и можно показать книгу пользователю.

...
private TextWidgetExt widget;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_reader);

  ...

  try {
    widget.setBook(BookLoader.fromFile(filepath, this, 1L));
    Book book = widget.controller().book;
    if (book != null) {
      widget.invalidate();
      widget.post(new Runnable() {
        @Override public void run() {
          widget.gotoPage(0);
          setTitle(book.getTitle());
        }
      });
    } else {
      errorView.setVisibility(View.VISIBLE);
    }
  } catch (BookException e) {
    e.printStackTrace();
    errorView.setVisibility(View.VISIBLE);
  }
}

Теперь, если запустить приложение, мы уже можем видеть содержимое книги, пролистывать его и работать с ним.

Однако это не весь функционал, на который способна данная SDK. Ранее мы добавляли файлы ресурсов: различные разметки, иконки и меню. Добавим меню в наше приложение, для этого в ReaderActivity.java переопределим метод onCreateOptionsMenu() и добавим в него следующий код.

@Override
public boolean onCreateOptionsMenu(final Menu menu) {
  getMenuInflater().inflate(R.menu.app, menu);
  return true;
}

Также переопределим методы onPrepareOptionsMenu() и onOptionsItemSelected() для работы с этим меню.

@Override
public boolean onPrepareOptionsMenu(final Menu menu) {
  SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
  searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextChange(String query) {
      return true;
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
      widget.searchInText(query);
      menu.findItem(R.id.menu_search).collapseActionView();
      return true;
    }
  });

  menu.findItem(R.id.menu_table_of_contents).setEnabled(TableOfContentsUtil.isAvailable(widget));
  String name = widget.colorProfile().name;
  menu.findItem(R.id.menu_color_profile_light).setChecked("defaultLight".equals(name));
  menu.findItem(R.id.menu_color_profile_dark).setChecked("defaultDark".equals(name));
  menu.findItem(R.id.menu_color_profile_dark_with_bg).setChecked("darkWithBg".equals(name));
  menu.findItem(R.id.menu_color_profile_pink).setChecked("pink".equals(name));
  return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
  BaseStyle baseStyle = widget.baseStyle();

  switch (item.getItemId()) {
    case R.id.menu_table_of_contents: {
      final Intent intent = TableOfContentsUtil.intent(widget);
      if (intent != null) {
        startActivityForResult(intent, REQUEST_TABLE_OF_CONTENT);
      }
      break;
    }
    case R.id.menu_zoom_in:
      baseStyle.fontSize.setValue(baseStyle.fontSize.getValue() + 2);
      break;
    case R.id.menu_zoom_out:
      baseStyle.fontSize.setValue(baseStyle.fontSize.getValue() - 2);
      break;
    case R.id.menu_color_profile_light:
      widget.setColorProfileName("defaultLight");
      break;
    case R.id.menu_color_profile_dark:
      widget.setColorProfileName("defaultDark");
      break;
    case R.id.menu_color_profile_dark_with_bg:
      widget.setColorProfileName("darkWithBg");
      break;
    case R.id.menu_color_profile_pink:
      widget.setColorProfileName("pink");
      break;
  }
  widget.clearTextCaches();
  widget.invalidate();
  return true;
}

Таким образом мы добавим в читалку меню, позволяющее нам:

  • Менять размер текста
  • Менять стиль текста
  • Искать по тексту
  • Открывать оглавление

По поводу оглавления стоит сказать отдельно. В коде, взятом из библиотеки, оглавление представлено в виде отдельной активности, мы её скопировали ранее, но не добавили в манифест. Поэтому откроем файл манифеста AndroidManifest.xml и зарегистрируем в нём новую активность.

<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=".TableOfContentsActivity"/>
</application>

Теперь при открытии оглавления через меню у нас будет запускаться новая активность, содержащая в себе оглавление открытой книги. Стоит отметить, что не все книги содержат такое оглавление, поэтому в таких случаях кнопка меню будет неактивна.

При выборе главы обратно в ReaderActivity будет возвращаться номер страницы, на которую нужно перейти. Чтобы получить этот результат, в ReaderActivity.java мы переопределим метод onActivityResult(), в который добавим следующий код.

public class ReaderActivity extends AppCompatActivity {
  private final int REQUEST_TABLE_OF_CONTENT = 1;

  ...

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
      case REQUEST_TABLE_OF_CONTENT:
        if (resultCode == RESULT_OK) {
          int ref = data.getIntExtra(String.valueOf(TableOfContentsUtil.Key.reference), -1);
          if (widget != null && ref != -1) {
            widget.jumpTo(new FixedPosition(ref, 0, 0));
          }
        }
      default:
        super.onActivityResult(requestCode, resultCode, data);
    }
  }

Как можно увидеть, библиотека предоставляет разработчикам весь свой функционал, единственным ограничением бесплатной версии здесь является то, что она открывает только первые несколько страниц книги, о чём говорится при открытии книги.

Если вы купили ключ SDK и хотите добавить его в своё приложение, вам понадобится в файле build.gradle модуля приложения добавить в блоке buildTypes следующий код.

buildTypes {
  ...

  all {
  resValue 'string', 'fbreader_sdk_key', 'put your fbreader sdk key here'
  }
}

Не смотря на то, что движок FBReader достаточно мощный, а также учитывая графические ресурсы, добавляемые вместе с библиотекой, итоговый APK получился небольшого размера, что говорит о хорошей оптимизации библиотеки.

 

Итак, с помощью FBReader SDK и небольшого количества кода мы смогли создать простую читалку для электронных книг. Можно уже оставить как есть, либо продолжить модифицировать, изменяя интерфейс и дизайн так, как нужно разработчику. Пока что SDK есть только на Android, но разработчики планируют к концу года выпустить версию и для iOS.

Пишем читалку электронных книг на Android с помощью FBReader SDK: 7 комментариев

  1. Izzat

    В начале написали бы что SDK платная )) причем оочень дорогая ))

  2. profdiman

    SDK не только платная, но еще и очень дорогая. 1000 долларов за лицензию на одно приложение это уж слишком.

  3. Владислав

    Я дико извиняюсь, но по вашему уроку библиотека совершенно точно не хочет подключаться. Можете, пожалуйста, уделить немного времени на помощь? Спасибо.

    Так же создал призыв о помощи на кибер форууме.
    https://www.cyberforum.ru/android-dev/thread2900216.html

Добавить комментарий для Валерий Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *