Я не профессиональный разработчик, хотя и учился на программиста. Сейчас работаю системным администратором и планирую переходить в разработчики. Пишу для себя приложение, которое парсит один из популярных сайтов тематики IT и показывает статьи в нативном приложении android. Захотелось, чтобы все картинки в статье открывались в галерее, как в мобильном приложении habr.

Скорее всего, руководство получится простым, но мне хотется получить feedback и, скорее всего, кому-то это руководство окажется полезным.

Я попробовал несколько плагинов. Несколько не запустились, другие требуют JQuery, а это замедляет загрузку страницы в приложении. В итоге, я выбрал плагин PhotoSwipe. плагин не требует JQuery, работает быстро и почти ничего не весит.

Для начала нужно подумать где в приложении лучше разместить исходные файлы плагина. Сначала по неопытности я размещал файлы в папку raw, но лучше размещать их в папку assets. Правой кнопкой мыши по app>new > folder>Assets Folder

Разметка будет одно простое WebView:

Код
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></WebView>

</android.support.constraint.ConstraintLayout>


Дальше нужно скачать или клонировать репозиторий с PhotoSwipe (ссылка на сайте плагина) и скопировать все содержимое папки dist в assets (dist можно переименовать в PhotoSwipe. Еще можно создать файл style.css в папке css, чтобы изображения занимали всю ширину.

img {
  width: 100%;
  height: auto;
}

Для галереи нужно подключить стили, javascipt файлы и файл стилей для скина. Можно сформировать html:

Здесь собирается html для WebView
public String getHTML() {

        String header = "<!DOCTYPE HTML><HTML><head><title>test</title>";

        // Вызываем скрипт, который подготавливает страницу к включению
        // плагина после полной загрузки страниицы.
        String headerEnd = "</head><body onload=\"init();\">";
        String footer = "</body></HTML>";

        String charset = "<meta charset=\"utf-8\">";

        String style = "<link href=\"css/style.css\" type=\"text/css\" rel=\"stylesheet\" />";
        String pscss = "<link rel=\"stylesheet\" href=\"PhotoSwipe/photoswipe.css\">";
        String psdscss = "<link rel=\"stylesheet\" href=\"PhotoSwipe/default-skin/default-skin.css\">";

        // Скрипт, который создает галерею после нажатия на изображение
        String script = "<script type=\"text/javascript\" src=\"js/script.js\"></script>";

        String psjs = "<script src=\"PhotoSwipe/photoswipe.js\"></script> ";
        String psuijs = "<script src=\"PhotoSwipe/photoswipe-ui-default.min.js\"></script>";

        StringBuilder stringBuilder = new StringBuilder();

        // Собираем head
        stringBuilder.append(header);
        stringBuilder.append(charset);
        stringBuilder.append(style);
        stringBuilder.append(pscss);
        stringBuilder.append(psdscss);
        stringBuilder.append(script);
        stringBuilder.append(psjs);
        stringBuilder.append(psuijs);

        stringBuilder.append(headerEnd);

        stringBuilder.append("<img src=\"https://ps.denko.me/images/linux1.jpg\" />");
        stringBuilder.append("<img src=\"https://ps.denko.me/images/linux2.jpg\" />");
        stringBuilder.append("<img src=\"https://ps.denko.me/images/linux3.jpeg\" />");

        stringBuilder.append(getPhotoSwipeHTML());
        stringBuilder.append(footer);

        return stringBuilder.toString();
.

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

Код желательно вставлять перед закрывающим тегом

Контейнер для галереи
public String getPhotoSwipeHTML() {
        return "<!-- Root element of PhotoSwipe. Must have class pswp. -->\n" +
                "<div class=\"pswp\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n" +
                "\n" +
                "    <!-- Background of PhotoSwipe. \n" +
                "         It's a separate element as animating opacity is faster than rgba(). -->\n" +
                "    <div class=\"pswp__bg\"></div>\n" +
                "\n" +
                "    <!-- Slides wrapper with overflow:hidden. -->\n" +
                "    <div class=\"pswp__scroll-wrap\">\n" +
                "\n" +
                "        <!-- Container that holds slides. \n" +
                "            PhotoSwipe keeps only 3 of them in the DOM to save memory.\n" +
                "            Don't modify these 3 pswp__item elements, data is added later on. -->\n" +
                "        <div class=\"pswp__container\">\n" +
                "            <div class=\"pswp__item\"></div>\n" +
                "            <div class=\"pswp__item\"></div>\n" +
                "            <div class=\"pswp__item\"></div>\n" +
                "        </div>\n" +
                "\n" +
                "        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->\n" +
                "        <div class=\"pswp__ui pswp__ui--hidden\">\n" +
                "\n" +
                "            <div class=\"pswp__top-bar\">\n" +
                "\n" +
                "                <!--  Controls are self-explanatory. Order can be changed. -->\n" +
                "\n" +
                "                <div class=\"pswp__counter\"></div>\n" +
                "\n" +
                "                <button class=\"pswp__button pswp__button--close\" title=\"Close (Esc)\"></button>\n" +
                "\n" +
                "                <button class=\"pswp__button pswp__button--share\" title=\"Share\"></button>\n" +
                "\n" +
                "                <button class=\"pswp__button pswp__button--fs\" title=\"Toggle fullscreen\"></button>\n" +
                "\n" +
                "                <button class=\"pswp__button pswp__button--zoom\" title=\"Zoom in/out\"></button>\n" +
                "\n" +
                "                <!-- Preloader demo https://codepen.io/dimsemenov/pen/yyBWoR -->\n" +
                "                <!-- element will get class pswp__preloader--active when preloader is running -->\n" +
                "                <div class=\"pswp__preloader\">\n" +
                "                    <div class=\"pswp__preloader__icn\">\n" +
                "                      <div class=\"pswp__preloader__cut\">\n" +
                "                        <div class=\"pswp__preloader__donut\"></div>\n" +
                "                      </div>\n" +
                "                    </div>\n" +
                "                </div>\n" +
                "            </div>\n" +
                "\n" +
                "            <div class=\"pswp__share-modal pswp__share-modal--hidden pswp__single-tap\">\n" +
                "                <div class=\"pswp__share-tooltip\"></div> \n" +
                "            </div>\n" +
                "\n" +
                "            <button class=\"pswp__button pswp__button--arrow--left\" title=\"Previous (arrow left)\">\n" +
                "            </button>\n" +
                "\n" +
                "            <button class=\"pswp__button pswp__button--arrow--right\" title=\"Next (arrow right)\">\n" +
                "            </button>\n" +
                "\n" +
                "            <div class=\"pswp__caption\">\n" +
                "                <div class=\"pswp__caption__center\"></div>\n" +
                "            </div>\n" +
                "\n" +
                "        </div>\n" +
                "\n" +
                "    </div>\n" +
                "\n" +
                "</div>";
    }


В документации приводится пример включения плагина по клику на изображение, но отмечается, что PhotoSwipe все равно как именно он будет запускаться. В итоге, я написал свой скрипт. Я показываю статью с чужого сайта в приложении, так что получаю уже готовый html. Чтобы динамически не работать с версткой, я в своем скрипте всем изображениям (потому что лишних в статье точно нет) меняю класс, присваиваю обработчик событий (при клике на который передается url — он служит идентификатором для галереи, и запускается плагин).

Плагину нужно знать размеры изображения. Поскольку галерея подключается когда вся страница загружена, узнать конечные размеры изображений просто, через свойства naturalWidth и naturalHeight изображения.

Код скрипта. Сначала при загрузке страницы запускается функция init(); Потом при клике на изображение openGallery().

Код
// Меняем всем изображением класс и вешаем обработчик событий
function init() {

	var images = document.images;

	for (var i = 0; i < images.length; i++) {
       
		images[i].className = "photoSwipe";
		images[i].onclick = function () { openGalery(this.attributes["src"].value); }
	}
}

// Собираем все необходимое
function openGalery(url) {

	var images =  document.getElementsByClassName("photoSwipe");
	var items = new Array();

	for (var i = 0; i < images.length; i++) {
        items.push(getAttributes(images[i]));
    }

    startPhotoSwipe(items, getIndex(images, url));
}

// Получаем аттрибуты изображения
function getAttributes(image) {

    return { 

        src: image.attributes["src"].value,
        w: image.naturalWidth,
        h: image.naturalHeight
    }
}
 
// Получаем номер изображения, на которое нажали
function getIndex(images, url) {

	for(var i = 0; i < images.length; i++) {

		if (url == images[i].attributes["src"].value)
		    return i;
	}

	return false;
}

// Запускаем плагин
function startPhotoSwipe(items, index) {

	var pswpElement = document.querySelectorAll('.pswp')[0];

    // define options (if needed)
    var options = {
        // optionName: 'option value'
        // for example:
        index: index, // start at index slide
        bgOpacity: 0.9,
        pinchToClose: false,
        fullscreenEl: false,
        closeEl:false,
        zoomEl: false,
        shareEl: false,
        indexIndicatorSep: ' из ',
        tapToClose: true
    };

    callAndroid.setGallery(true); // Эта строчка нужно для Java кода андроид, об этом ниже

    // Initializes and opens PhotoSwipe
    var gallery = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, items, options);
    gallery.init();
}


Дальше необходимо настроить WebView и загрузить страницу.

webView = findViewById(R.id.webView);
            webView.setWebViewClient(new WebViewClient());
            webView.setWebChromeClient(new WebChromeClient());
            webView.getSettings().setJavaScriptEnabled(true);
            webView.getSettings().setDomStorageEnabled(true);

На этом этапе страница загружается, плагин стартует при нажатии на изображение, но при нажатии кнопки назад при открытой галерее закрывается все приложение, а не плагин.

Чтобы это исправить я сделал мост Javascipt.

Для начала нужно сделать singleton с состоянием галереи.

Код
package me.denko.photoswipe;

class GalleryState {
    private static final GalleryState ourInstance = new GalleryState();

    static GalleryState getInstance() {
        return ourInstance;
    }

    private GalleryState() {
    }

    private boolean isGallery = false;

    public boolean isGallery() {
        return isGallery;
    }

    public void setGallery(boolean gallery) {
        isGallery = gallery;
    }
}


Дальше интерфейс для вызова из WebView

Код
package me.denko.photoswipe;

import android.webkit.JavascriptInterface;

public class JavascriptFromWebView {

    @JavascriptInterface
    public static void setGallery(boolean bool) {

        GalleryState.getInstance().setGallery(bool);
    }
}


Нужно подключить этот интерфейс к WebView

webView.addJavascriptInterface(new JavascriptFromWebView(), "callAndroid");

В скрипт открытия галереи нужно дописать
callAndroid.setGallery(true);


В скрипт закрытия галереи в файле photoswipe.js

Код
close: function() {
		if(!_isOpen) {
			return;
		}

		callAndroid.setGallery(false); // Дописывается эта строка

		_isOpen = false;
		_isDestroying = true;
		_shout('close');
		_unbindEvents();

		_showOrHide(self.currItem, null, true, self.destroy);
	},


Теперь при открытии галереи состоянию присваивается true, при закрытии false. Осталось только добавить обрабочик нажатия кнопки назад WebView.

Код
webView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {

                if (event.getAction() != KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {
                    if (GalleryState.getInstance().isGallery()) {
                        webView.reload();
                        GalleryState.getInstance().setGallery(false);
                    } else
                        onBackPressed();
                }

                return true;
            }
        });


Теперь галерею можно открыть и потом закрыть кнопкой назад.

Полный код приложения на github.

Критика кода приветствуется. Если я что-то сделал не так, прошу написать об этом в комментариях.

Комментарии (2)


  1. STFBEE
    04.06.2019 09:32

    Простите, но что тут нативного?
    Кроме вызова метода js из java кода, что делается одним копипастом с so.

    Кстати, слушатель события клавиш, емнип, не вызовется, если фокус будет на каком-то другом элементе, например на Options Menu (но это не точно, нужно проверить). Хорошей практикой все же будет переопределение onBackPressed() и встраивание логики обработки «назад» туда.


    1. also_east Автор
      04.06.2019 11:20

      Простите, но что тут нативного?
      Кроме вызова метода js из java кода, что делается одним копипастом с so.

      Про нативность я писал, когда говорил о своем приложении. Оно парсит сайт, показывает статьи в приложении android так, как настроено. Это, мне кажется, нативность. У меня все сложнее, я специально оставил только то, что нужно, чтобы подключить плагин.
      Кстати, слушатель события клавиш, емнип, не вызовется, если фокус будет на каком-то другом элементе, например на Options Menu (но это не точно, нужно проверить). Хорошей практикой все же будет переопределение onBackPressed() и встраивание логики обработки «назад» туда.

      У меня оно показывается через ViewPager и фрагменты, без меню, так что фокус всегда на WebView. Единственное, нужно было еще создать свой класс наследник от ViewPager и переопределить
      @Override
          public boolean onInterceptTouchEvent(MotionEvent event) {
              if (!AppState.getInstance().isGallery()) {
                  return super.onInterceptTouchEvent(event);
              }
      
              return false;
          }


      Иначе если скроллить с картинки, оно работало, но если начинать скроллить от края экрана, открывался соседний фрагмент