Превращаем html в нативные компоненты

в 13:28, , рубрики: android, android development, Блог компании Surfingbird, Разработка под android

Доброго дня! Мы, мобильные разработчики компании surfingbird, решили попробовать написать небольшой цикл статей о том с какими трудностями мы сталкиваемся в процессе разработки мобильных приложений (android, ios), и как мы их решаем. Первый пост мы решили посвятить проблеме webview. Сразу оговорюсь, что решили мы эту проблему несколько кардинально… Для того чтобы было более понятно, придется рассказать пару слов о собственно том, чем мы занимаемся. Мы агрегируем контент из различных источников (парсим оригинальные статьи), выделяем значимую часть (контент) и на основе оценок пользователя и всяких сложных алгоритмов рекомендуем их конечному пользователю ну и конечно просто отображаем в более удобном виде.

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

Но при использовании webview для отображения контента мы столкнулись с рядом сложностей. Этот компонент тяжело поддается кастомизации и довольно тяжел и даже, я бы сказал, глючен. Настал день, когда мы поняли, что не хотим больше видеть webview вообще. Но избавиться от него, учитывая то, что контент у нас отдается в html — оказалось не так-то просто. Поэтому мы решили превратить html в нативные компоненты.

image

Попробую в двух словах описать принцип, прежде чем переходить к примерам кода.

  1. Чистим html от верстки и яваскриптов
  2. В качестве опорной точки мы используем ссылки на изображения и iframe
  3. Все что до и между ссылками на изображения — это текст, который рендерим c помощью textview
  4. Непосредственно изображения — рендерим c помощью imageview
  5. Для Iframe — анализируем содержимое и видео рендерим как кликабельные картинки на видео, а прочее рендерим как ссылки или, в крайнем случае — вставляем в контейнер webview (например, ссылки на аудио с soundcloud)
  6. Получившийся массив компонентов помещаем в listview и адаптер (на самом деле уже в recyclerView, но на момент написания статьи это был listview)

Первым делом необходимо очистить html от всякого мусора в виде javascript и css. Для этих целей мы воспользовались библиотекой HtmlCleaner. Заодно создадим массив всех изображений, которые встречаются в контенте (он понадобится нам позже):

    final ArrayList<Link> links = new ArrayList<Link>();
    HtmlCleaner mHtmlCleaner = new HtmlCleaner();

    CleanerTransformations transformations =
            new CleanerTransformations();
    TagTransformation tt = new TagTransformation("img", "imgs", true);
    transformations.addTransformation(tt);
    mHtmlCleaner.setTransformations(transformations);
    //clean
    html = mHtmlCleaner.getInnerHtml(mHtmlCleaner.clean(parsed_content));

    TagNode root = mHtmlCleaner.clean(html);

    root.traverse(new TagNodeVisitor() {
        @Override
        public boolean visit(TagNode tagNode, HtmlNode htmlNode) {
            if (htmlNode instanceof TagNode) {
                TagNode tag = (TagNode) htmlNode;
                String tagName = tag.getName();
                if ("iframe".equals(tagName)) {
                    if (tag.getAttributeByName("src") != null) {
                        Link link = parseTag(tag, "iframe");
                        if (link != null) {
                            links.add(link);
                        }
                    }
                }
                if ("imgs".equals(tagName)) {
                    String src = tag.getAttributeByName("src");
                    //ico
                    if (src != null && !src.endsWith("/") && !src.toLowerCase().endsWith("ico")) {
                        Link link = parseTag(tag, "img");
                        if (link != null) {
                            links.add(link);
                        }
                    }
                }
            }
            return true;
        }
    });

Здесь мы заменяем теги img на imgs^_^, во первых, чтобы у textview не было соблазна отрендерить картинки, во вторых, чтобы затем найти все ссылки на картинки и заменить их на imageview.

Раз уж мы решили отображать картинки нативно, то не плохо было бы заодно увеличить их, чтобы средние картинки, например более 1/3 экрана — стали на весь экран смартфона, мелкие картинки — стали более крупными, а совсем маленькими — можно совсем пренебречь (как правило это иконки ссылок на соцсети):

public Link parseTag(TagNode tag,String type) {
        final String src = tag.getAttributeByName("src");
        final String width = tag.getAttributeByName("width");
        final String height = tag.getAttributeByName("height");
        int iWidth=0, iHeight=0;
        try {
            iWidth = Integer.parseInt(width.split("\.")[0]);
            iHeight = Integer.parseInt(height.split("\.")[0]);
        }
        catch (Exception e) {}
        //если картинка больше 1/3 экрана - тянем пропорционально
        if (iWidth>((displayWidth*1)/3) && iHeight>0) {

            iHeight = (displayWidth * iHeight)/iWidth;
            iWidth = displayWidth;
        }
        //выкидываем мелкие пиписьки
        if (iWidth>45 && iHeight>45) {
            int scaleFactor = 1;
            if (iWidth<displayWidth/3) {
                //умножаем на 2 средние картинки
                scaleFactor = 2;
            }
            if (iHeight>=4096 || iWidth>=4096 || src.endsWith("gif")) {
                type = "iframe";
            }
            return new Link(type, src, iWidth*scaleFactor, iHeight*scaleFactor,"");
        }
        return null;
    }

Собственно, половина работы уже сделано. Теперь осталось пройти по массиву линков на изображения, найти контент до изображения и вставить его в textview, после этого вставить картинку.
Для этого мы создали ArrayList в который будем помещать собственно сам контент, с указанием его типа (текст, картинка, iframe).

Некий псевдокод:

    private ArrayList<Link> data = new ArrayList<Link>();;
    for(int i=0;i<links.size();i++) {
        final Link link = links.get(i);
        if (link.type.equals("txt")) continue;
        int pos = html.indexOf(link.src);
        String abzats = "";
        if (pos>0) {
            abzats = html.substring(0, pos);
            int closeTag = html.indexOf(">",pos)+1;
            if (closeTag>0) {
                html = html.substring(closeTag);
            }
            if (!TextUtils.equals("", abzats)) {
                data.add(new Link("txt","",0,0,abzats));
            }
        }
        //add text

        if (link.type.equals("img")) {
            //add image
            data.add(link);
        }

        //add iframe
        if (link.type.equals("iframe")) {
            data.add(link);
        }
    }
    data.add(new Link("txt","",0,0,html));

На этом месте у нас есть великолепный массив, с контентом, разбитым на типы. Все что осталось — отрендерить его. А для рендеренга массивов сложно найти что то более прекрасное чем обычный listview + adapter:
Примерно так выглядит код getView в адаптере:

if (link.type.equals("txt")) {
  //текст
           return getTextView(activity, link.txt);
}
if (link.type.equals("img")) {
 // картинка
}
...
//где, textview 
public TextView getTextView(Context context,String txt){
        TextView textView = new TextView(activity);
        textView.setMovementMethod(LinkMovementMethod.getInstance());
        textView.setText(Html.fromHtml(txt));
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP,fontSize);
        textView.setPadding(UtilsScreen.dpToPx(8),0,UtilsScreen.dpToPx(8),0);
        textView.setAutoLinkMask(Linkify.ALL);
        textView.setLineSpacing(0, 1.4f);
        ColorStateList cl = null;
        try {
            XmlResourceParser xpp = context.getResources().getXml(R.xml.textview_link_color_selector);
            cl = ColorStateList.createFromXml(context.getResources(), xpp);
            textView.setLinkTextColor(cl);
        } catch (Exception e) {
            textView.setLinkTextColor(Color.parseColor("#6fb304"));
        }

        return textView;
    }

Итак, текст рендерится как html с помощью textview, картинки превращаются в обычные картинки, но оптимизированные под разрешение устройства. Осталась только боль с iframe. Мы анализируем его содержимое, и если это ссылка на youtube, например — генерируем картинку с плейсхолдером видео, на клик по которой открываем приложение youtube. Вобщем, тут все уже совсем просто:

    String youtubeVideo = "";
    if (link.src.contains("lj-toys") && link.src.contains("youtube") && link.src.contains("vid=")) {
        try {
            youtubeVideo = link.src.substring(link.src.indexOf("vid=") + 4, link.src.indexOf("&", link.src.indexOf("vid=") + 4));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //http://www.youtube.com/embed/ZSPyC6Uv9xw
    if (link.src.contains("youtube") && link.src.contains("embed/")) {
        try {
            youtubeVideo = link.src.substring(link.src.indexOf("embed/") + 6);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    if (!youtubeVideo.equals("")) {
        //new RelativeLayout
        RelativeLayout relativeLayout = new RelativeLayout(activity);
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);

        ImageView imageView = new ImageView(activity);
        imageView.setLayoutParams(layoutParams);
        relativeLayout.addView(imageView);

        imageView.setBackgroundColor(Color.parseColor("#f8f8f8"));
        if (link.width>0 && link.height>0) {
            aq.id(imageView).width(link.width, false).height(link.height, false);
        }
        String youtubeVideoImage = youtubeVideo;
        if (youtubeVideoImage.contains("?")) {
            //params
            youtubeVideoImage = youtubeVideoImage.substring(0, youtubeVideoImage.indexOf("?"));
        }
        if (link.width>0) {
            aq.id(imageView).image("http://img.youtube.com/vi/" + youtubeVideoImage + "/0.jpg", true, false, link.width, 0, null, AQuery.FADE_IN_NETWORK);
        }
        else {
            aq.id(imageView).image("http://img.youtube.com/vi/" + youtubeVideoImage + "/0.jpg");
        }


        ImageView imageViewPlayBtn = new ImageView(activity);
        relativeLayout.addView(imageViewPlayBtn);

        RelativeLayout.LayoutParams playBtnParams = new RelativeLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

        playBtnParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
        imageViewPlayBtn.setLayoutParams(playBtnParams);
        aq.id(imageViewPlayBtn).image(R.drawable.play_youtube);


        final String videoId = youtubeVideo;
        aq.id(relativeLayout).clickable(true).clicked(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {

                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube:" + videoId));
                    intent.putExtra("VIDEO_ID", videoId);
                    activity.startActivity(intent);
                } catch (Exception e) {
                    activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.youtube.com/watch?v=" + videoId)));
                }
            }
        });
        return relativeLayout;

Мы сняли небольшое видео, с демонстрацией приложения в работе, но лучше конечно скачать приложение и попробовать самому.

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

Автор: recompileme

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js