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

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

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

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

Чистим html от верстки и яваскриптов В качестве опорной точки мы используем ссылки на изображения и iframe Все что до и между ссылками на изображения — это текст, который рендерим c помощью textview Непосредственно изображения — рендерим c помощью imageview Для Iframe — анализируем содержимое и видео рендерим как кликабельные картинки на видео, а прочее рендерим как ссылки или, в крайнем случае — вставляем в контейнер webview (например, ссылки на аудио с soundcloud) Получившийся массив компонентов помещаем в listview и адаптер (на самом деле уже в recyclerView, но на момент написания статьи это был listview) Первым делом необходимо очистить html от всякого мусора в виде javascript и css. Для этих целей мы воспользовались библиотекой HtmlCleaner. Заодно создадим массив всех изображений, которые встречаются в контенте (он понадобится нам позже):

final ArrayList links = new ArrayList(); 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.

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

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) {} //если картинка больше ⅓ экрана — тянем пропорционально if (iWidth>((displayWidth*1)/3) && iHeight>0) {

iHeight = (displayWidth * iHeight)/iWidth; iWidth = displayWidth; } //выкидываем мелкие пиписьки if (iWidth>45 && iHeight>45) { int scaleFactor = 1; if (iWidth=4096 || iWidth>=4096 || src.endsWith («gif»)) { type = «iframe»; } return new Link (type, src, iWidth*scaleFactor, iHeight*scaleFactor,»); } return null; } Собственно, половина работы уже сделано. Теперь осталось пройти по массиву линков на изображения, найти контент до изображения и вставить его в textview, после этого вставить картинку.Для этого мы создали ArrayList в который будем помещать собственно сам контент, с указанием его типа (текст, картинка, iframe).

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

private ArrayList data = new ArrayList();; for (int i=0; i0) { 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.

© Habrahabr.ru