Доброго дня! Мы, мобильные разработчики компании surfingbird, решили попробовать написать небольшой цикл статей о том с какими трудностями мы сталкиваемся в процессе разработки мобильных приложений (android, ios), и как мы их решаем. Первый пост мы решили посвятить проблеме webview. Сразу оговорюсь, что решили мы эту проблему несколько кардинально… Для того чтобы было более понятно, придется рассказать пару слов о собственно том, чем мы занимаемся. Мы агрегируем контент из различных источников (парсим оригинальные статьи), выделяем значимую часть (контент) и на основе оценок пользователя и всяких сложных алгоритмов рекомендуем их конечному пользователю ну и конечно просто отображаем в более удобном виде.
В мобильных приложениях мы стремимся не только очистить страницы от элементов верстки и назойливых всплывающих окон, но и оптимизировать контент для потребления на мобильных устройствах.
Но при использовании webview для отображения контента мы столкнулись с рядом сложностей. Этот компонент тяжело поддается кастомизации и довольно тяжел и даже, я бы сказал, глючен. Настал день, когда мы поняли, что не хотим больше видеть webview вообще. Но избавиться от него, учитывая то, что контент у нас отдается в html — оказалось не так-то просто. Поэтому мы решили превратить html в нативные компоненты.
Попробую в двух словах описать принцип, прежде чем переходить к примерам кода.
- Чистим html от верстки и яваскриптов
- В качестве опорной точки мы используем ссылки на изображения и iframe
- Все что до и между ссылками на изображения — это текст, который рендерим c помощью textview
- Непосредственно изображения — рендерим c помощью imageview
- Для Iframe — анализируем содержимое и видео рендерим как кликабельные картинки на видео, а прочее рендерим как ссылки или, в крайнем случае — вставляем в контейнер webview (например, ссылки на аудио с soundcloud)
- Получившийся массив компонентов помещаем в 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