Pull to refresh

Пишем клиент для Хабра под Android

Reading time 18 min
Views 42K
Забегая вперед, вот что получилось:
image


12:56. Я буду делать это параллельно с написанием топика (так интересней). По ходу написания клиента поясняя все шаги. Итак, покурили, налили чай, подготовили плейлист и, пока чай остывает — проверяем не занято ли имя habrahabr в маркете. Отлично, переходим к созданию приложения.

13:02 Создаем новый проект.
Скриншот

API level равен 4, по той причине, что при меньшем значении — на планшетниках Samsung Galaxy Tab разрешение экрана будет некорректным и обладатели данных чудо-девайсов не преминут насовать вам кучу минусов в маркет (хотя в принципе, врятли это косяк разработчика).

13:08 Фиксим манифест.
Необходимо добавить две строчки:
— android:configChanges=«orientation», данная строка нужна для того, чтобы при смене ориентации экрана не разрушалось наше активити.
— <uses-permission android:name=«android.permission.INTERNET» />, запрашиваем разрешение на доступ в интернет
AndroidManifest
* играет front242 — headhunter v3.0

13:13 Фиксим layout.
Стираем всё и добавляем один единственный элемент — webview — на весь экран

13:16 Иконка.
При помощи фаербага подрезаем абсолютную ссылку и при помощи фотошопа обрезаем до 48*48 px и кидаем в res/drawable…

13:27 С иконкой всё сложнее оказалось. Пятно логотипа после уменьшения превратилось в мутную хрень, пришлось нагуглить. Надеюсь автор не обидится.

Уф, самое сложное закончили, наконец то можно покодить.

13:39 Загружаем хабр
public class habr extends Activity {
  
  private WebView wv;
  
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    
    wv = (WebView) findViewById(R.id.wv);
    
    WebSettings webSettings = wv.getSettings();
    webSettings.setSavePassword(true);
    webSettings.setSaveFormData(true);
    webSettings.setJavaScriptEnabled(true);
    
    wv.loadUrl("http://habrahabr.ru");
  }
}


* This source code was highlighted with Source Code Highlighter.


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

13:53 Продолжаем разговор.

public class habr extends Activity {
  
  private WebView wv;
  private String LASTURL = "";
  
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    this.getWindow().requestFeature(Window.FEATURE_PROGRESS);
    setContentView(R.layout.main);
    
    wv = (WebView) findViewById(R.id.wv);
    
    WebSettings webSettings = wv.getSettings();
    webSettings.setSavePassword(true);
    webSettings.setSaveFormData(true);
    webSettings.setJavaScriptEnabled(true);
    
    final Activity activity = this;
    
    wv.setWebChromeClient(new WebChromeClient() {
      public void onProgressChanged(WebView view, int progress)
      {
        activity.setTitle(" "+LASTURL);
        activity.setProgress(progress * 100);

        if(progress == 100)
          activity.setTitle(" "+LASTURL);
      }
    });
    wv.setWebViewClient(new WebViewClient() {
      public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        Toast.makeText(getApplicationContext(), "Error: " + description+ " " + failingUrl, Toast.LENGTH_LONG).show();
      }
      
      @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url)
      {
        if (url.indexOf("habrahabr")<=0) {
          // the link is not for a page on my site, so launch another Activity that handles URLs
          Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
          startActivity(intent);
          return true;
        }
        return false;
      }

      public void onPageStarted (WebView view, String url, Bitmap favicon) {
        LASTURL = url;
      }
      
      public void onPageFinished (WebView view, String url) {

      }
    });
    
    wv.loadUrl("http://habrahabr.ru");
  }
}


* This source code was highlighted with Source Code Highlighter.


Итак, нам нужен градусник загрузки.
1. Запрашиваем фичу: this.getWindow().requestFeature(Window.FEATURE_PROGRESS);
2. На прогрессчейндж — заполняем окно градусника
activity.setTitle(" "+LASTURL);
activity.setProgress(progress * 100);
if(progress == 100) activity.setTitle(" "+LASTURL);
3. На начало загрузки запоминаем url в переменной LASTURL = url;

Обрабатываем отвалившийся вайфай на онресивед еррор:
Toast.makeText(getApplicationContext(), «Error: » + description+ " " + failingUrl, Toast.LENGTH_LONG).show();
(всплывает сообщение для совсем уж дебилов, хотя итак всё будет на странице написано )

14:05 Фиксим вёрстку.
Немного теории. На загруженной странице можно выполнить джаваскрипт. Если например набрать в адресной строке браузера что то вроде javasсript:alert(document.body.innerHTML) — то мы увидим тело страницы (копипастерам — в примере выше — буква «с» — русская, чтобы парсер пропустил).
Ну а дальше — дерево ДОМ и т.д., твори что хочешь, хоть полностью страницу переделывай. Однако мы не зайдем так далеко (я надеюсь) и просто скроем сайдбар, для улучшения читабельности. А браузер уже сам растянет полезный контент по странице. Итак, пробуем добавить обработчик на окончание загрузки страницы:

public void onPageFinished (WebView view, String url) {
     view.loadUrl("javascript:(function() { " +
        "hide('sidebar');"+
        "function hide(id){if (document.getElementById(id)){document.getElementById(id).style['display'] = 'none';}}"+
            "})()");
}


* This source code was highlighted with Source Code Highlighter.


/* Отвлекли по работе */
Такс, сайдбар скрывается, но с неким скачком. Фиксим загрузку изображений:

14:34 Ускоряем загрузку
Для этого отключим картинки при старте страницы:
view.getSettings().setLoadsImagesAutomatically(false);
и включим на финише, после хака с джаваскриптом:
view.getSettings().setLoadsImagesAutomatically(true);

Такс, грузить контент стал ощутимо быстрее (на моей полуживой Йоте, в эмуляторе, по крайней мере)

14:37 Обед

15:23 Продолжаем разговор
Пока ходил на обед — заглянул в зону бесплатного вайфая, заодно и потестил. С сожалением узрел панель поиска, нелепо висящую в пустом правом углу. Попробуем с ней чтоть сделать.
Для начала тупо скроем.

* музыка: submatakana — the krypt (это что то с чем то)

"hideByClass('panel-tools');"+
"function hideByClass(c){var e=document.getElementsByClassName(c);for(var i=0;i<e.length;i++){e[i].style['display'] = 'none';}}"+


* This source code was highlighted with Source Code Highlighter.


Мда, ломать не строить. Такс, попробуем «приаппендить её к списку блогов.

"var parent = document.getElementsByClassName('page-navigation')[0];"+
            "var panel = document.getElementsByClassName('panel-tools')[0];"+
            "var div = document.createElement('div');"+
            "div.innerHTML = panel.innerHTML;"+
            "parent.appendChild(div);"+


* This source code was highlighted with Source Code Highlighter.


опс, теперь у нас две панели)
"panel.innerHTML = '';"+
"div.style['margin-left'] = '30px;'"+


* This source code was highlighted with Source Code Highlighter.


15:57 Опс, одна кавычечка не там и весь скрипт рушится как карточный домик.
Надо так: div.style['margin-left'] = '30px';
Так попробую ещё вырубить рекламные блоки в меню, но нет так нет, что то я долго вожусь с грешной вёрсткой (ненавижу).

16:04 Так как данные элементы не поименованы — попробовал так:
"var urls=document.getElementsByTagName('a');for(var i=0;i<urls.length;i++){if (urls[i].target='_top'){urls[i].appendChild(document.createTextNode(''));}}"+

* This source code was highlighted with Source Code Highlighter.

Не срослось

16:16 Попробуем ударить по площадям:
"var imgs=document.getElementsByTagName('IMG');for(var i=0;i<imgs.length;i++){if (imgs[i].height=60) {imgs[i].src='';imgs[i].width=0;} }"+

* This source code was highlighted with Source Code Highlighter.

Картинки скрылись, но пустое место всё равно торчит( Ладно, пусть это останется на домашнее задание желающим. Пусть пока живут на радость рекламодателям.
Перекур.

16:45 Поиск, пожалуй вернем назад, немного ужав по ширине:
"var panel = document.getElementById('search');"+
"panel.style['width'] = '55px';"+


* This source code was highlighted with Source Code Highlighter.


Ну и займемся наконец андроидом.
16:47 Перекрываем аппаратную кнопку назад.

  1.     @Override
  2.   public boolean onKeyDown(int keyCode, KeyEvent event) {
  3.     if ((keyCode == KeyEvent.KEYCODE_BACK) && wv.canGoBack()) {
  4.       wv.goBack();
  5.       return true;
  6.     }
  7.     return super.onKeyDown(keyCode, event);
  8.   }
* This source code was highlighted with Source Code Highlighter.


16:57 Создаем меню

  1.   @Override
  2.   public boolean onCreateOptionsMenu(Menu menu)
  3.   {
  4.     super.onCreateOptionsMenu(menu);
  5.     
  6.     this.myMenu = menu;
  7.     MenuItem item = menu.add(0, 1, 0, "MAIN PAGE");
  8.     item.setIcon(R.drawable.home);
  9.     MenuItem item2 = menu.add(0, 2, 0, "BACK");
  10.     item2.setIcon(R.drawable.arrowleft);
  11.     MenuItem item3 = menu.add(0, 3, 0, "F5");
  12.     item3.setIcon(R.drawable.s);
  13.     MenuItem item4 = menu.add(0, 4, 0, "CLEAR CACHE");
  14.     item4.setIcon(R.drawable.trash);
  15.     MenuItem item5 = menu.add(0, 5, 0, "VOID");
  16.     item5.setIcon(R.drawable.vote);
  17.     return true;
  18.   }
  19.   
  20.   @Override
  21.   public boolean onOptionsItemSelected(MenuItem item)  {
  22.     switch (item.getItemId())
  23.     {
  24.       case 1:
  25.         wv.loadUrl("http://habrahabr.ru");
  26.         break;
  27.       case 2:
  28.         if (wv.canGoBack()) {
  29.           wv.goBack();
  30.         }
  31.         break;
  32.       case 3:
  33.         wv.loadUrl(LASTURL);
  34.         break;
  35.       case 4:
  36.         wv.clearCache(true);
  37.         break;
  38.       case 5:
  39.         Intent marketIntent2 = new Intent(Intent.ACTION_VIEW, Uri.parse(
  40.             "http://market.android.com/details?id=" + getPackageName()));
  41.           startActivity(marketIntent2);                
  42.         break;
  43.     }
  44.  
  45.     return true;
  46.   }
* This source code was highlighted with Source Code Highlighter.


Здесь пояснять особо даже и ничего вроде…
Вебвью хранит данные в локальном изолированном кеше и функция clearCache — удаляет закешированные картики и т.п.

В маркет пользователя отправляем при помощи интента + старт активити, это стандартный механизм взаимодействия с внешними приложениями.

17:01 Такс, пожалуй, стоит сделать режим с картиками/без картинок
Фиксим меню:
menu.add(0, 6, 0, „IMG ON“);
menu.add(0, 7, 0, „IMG OFF“);

17:05 Мутим функции сохранения настроек
  1. private void saveSettings(Boolean val)
  2.   {
  3.     SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  4.     SharedPreferences.Editor editor = settings.edit();
  5.     editor.putBoolean("IMGMODE", val);
  6.     editor.commit();
  7.   }
* This source code was highlighted with Source Code Highlighter.


В неё будем передавать настройки (грузить или нет картинки), а она пусть запихивает в переменную переданное значение.
(выше объявили константу PREFS_NAME — это как бы имя конфига)

Теперь просто вызываем её в обработчике меню:
  1. case 6:
  2.         saveSettings(true);        
  3.         break;
  4.       case 7:
  5.         saveSettings(false);                
  6.         break;
* This source code was highlighted with Source Code Highlighter.


17:18 И читаем константу при создании приложения из нашего конфига
  1. SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  2.     imgOn = settings.getBoolean("IMGMODE", false);
  3.     webSettings.setLoadsImagesAutomatically(imgOn);
* This source code was highlighted with Source Code Highlighter.


17:19 Тестируем ещё раз

17:25 Вроде косяков нет. Экспортируем проект.
В эклипсе это правая кнопка на проекте экспорт и запускается мастер, позволяющий создать/выбрать сертификат и упаковывающий проект. Далее топаем в маркет

17:38 Публикуем.
На самом деле не самая простая задача. Где то надо добыть кучу промографики определенного размера, поэтому не дизайнерам тут тяжко.

По параметрам.
— Чтобы установить основным языком приложения русский — надо сперва добавить русский, и только после этого появится возможность удалить английский.
— В полях дескрипшн и промотекст желательно упомянуть ключевые слова, по которым могут искать приложение.
— Если устновить цену free — потом сделать его платным — невозможно
— Не ставьте галку copy protection, приложение не будет ставиться на часть девайсов
— Лучше укажите все страны, даже если приложение только для русскоязычных, например.

17:50 Опубликовали.

Скачать с маркета
Скачать исходный код

Мда, с иконкой не очень красиво получилось( Почему то меня гложет эта мысль. Так что если кто-ть может изобразить чтоть 48*48 — буду премного благодарен.

Полностью, итоговый исходник:
  1. package ru.habrahabr.android;
  2.  
  3. import android.app.Activity;
  4. import android.content.Intent;
  5. import android.content.SharedPreferences;
  6. import android.graphics.Bitmap;
  7. import android.net.Uri;
  8. import android.os.Bundle;
  9. import android.view.KeyEvent;
  10. import android.view.Menu;
  11. import android.view.MenuItem;
  12. import android.view.Window;
  13. import android.webkit.WebChromeClient;
  14. import android.webkit.WebSettings;
  15. import android.webkit.WebView;
  16. import android.webkit.WebViewClient;
  17. import android.widget.Toast;
  18.  
  19. public class habr extends Activity {
  20.   
  21.   private WebView wv;
  22.   private String LASTURL = "";
  23.   Menu myMenu = null;
  24.   private static final String PREFS_NAME = "MyPrefs";
  25.   private Boolean imgOn;
  26.   
  27.   /** Called when the activity is first created. */
  28.   @Override
  29.   public void onCreate(Bundle savedInstanceState) {
  30.     super.onCreate(savedInstanceState);
  31.     this.getWindow().requestFeature(Window.FEATURE_PROGRESS);
  32.     setContentView(R.layout.main);
  33.     
  34.     wv = (WebView) findViewById(R.id.wv);
  35.     
  36.     WebSettings webSettings = wv.getSettings();
  37.     webSettings.setSavePassword(true);
  38.     webSettings.setSaveFormData(true);
  39.     webSettings.setJavaScriptEnabled(true);
  40.     
  41.     SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  42.     imgOn = settings.getBoolean("IMGMODE", false);
  43.     webSettings.setLoadsImagesAutomatically(imgOn);
  44.     
  45.     final Activity activity = this;
  46.     
  47.     wv.setWebChromeClient(new WebChromeClient() {
  48.       public void onProgressChanged(WebView view, int progress)
  49.       {
  50.         activity.setTitle(" "+LASTURL);
  51.         activity.setProgress(progress * 100);
  52.         if(progress == 100)
  53.           activity.setTitle(" "+LASTURL);
  54.       }
  55.     });
  56.     wv.setWebViewClient(new WebViewClient() {
  57.       public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
  58.         Toast.makeText(getApplicationContext(), "Error: " + description+ " " + failingUrl, Toast.LENGTH_LONG).show();
  59.       }
  60.       
  61.       @Override
  62.         public boolean shouldOverrideUrlLoading(WebView view, String url)
  63.       {
  64.         if (url.indexOf("habrahabr")<=0) {
  65.           // the link is not for a page on my site, so launch another Activity that handles URLs
  66.           Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
  67.           startActivity(intent);
  68.           return true;
  69.         }
  70.         return false;
  71.       }
  72.  
  73.       public void onPageStarted (WebView view, String url, Bitmap favicon) {
  74.         LASTURL = url;
  75.         view.getSettings().setLoadsImagesAutomatically(false);
  76.       }
  77.       
  78.       public void onPageFinished (WebView view, String url) {
  79.         view.loadUrl("javascript:(function() { " +
  80.             "hide('sidebar');"+
  81.             //"var parent = document.getElementsByClassName('page-navigation')[0];"+
  82.             //"var panel = document.getElementsByClassName('panel-tools')[0];"+
  83.             //"var div = document.createElement('div');"+
  84.             //"div.innerHTML = panel.innerHTML;"+
  85.             //"parent.appendChild(div);"+
  86.             //"panel.innerHTML = '';"+
  87.             //"div.style['margin-left'] = '31px';"+
  88.             "var panel = document.getElementById('search');"+
  89.             "panel.style['width'] = '55px';"+
  90.  
  91.             //"var imgs=document.getElementsByTagName('IMG');for(var i=0;i<imgs.length;i++){if (imgs[i].height=60) {imgs[i].src='';imgs[i].width=0;} }"+
  92.             //"var urls=document.getElementsByTagName('li');for(var i=0;i<urls.length;i++){if (urls[i].style='margin: -14px 0pt 0pt;'){urls[i].style['display'] = 'none';}}"+
  93.             //"hideByClass('panel-tools');"+
  94.             "function hide(id){if (document.getElementById(id)){document.getElementById(id).style['display'] = 'none';}}"+
  95.             //"function hideByClass(c){var e=document.getElementsByClassName(c);for(var i=0;i<e.length;i++){e[i].style['display'] = 'none';}}"+
  96.             "})()");
  97.         if (imgOn) view.getSettings().setLoadsImagesAutomatically(true);
  98.       }
  99.     });
  100.     
  101.     wv.loadUrl("http://habrahabr.ru");
  102.   }
  103.   
  104.   @Override
  105.   public boolean onKeyDown(int keyCode, KeyEvent event) {
  106.     if ((keyCode == KeyEvent.KEYCODE_BACK) && wv.canGoBack()) {
  107.       wv.goBack();
  108.       return true;
  109.     }
  110.     return super.onKeyDown(keyCode, event);
  111.   }
  112.   
  113.   @Override
  114.   public boolean onCreateOptionsMenu(Menu menu)
  115.   {
  116.     super.onCreateOptionsMenu(menu);
  117.     
  118.     this.myMenu = menu;
  119.     MenuItem item = menu.add(0, 1, 0, "MAIN PAGE");
  120.     item.setIcon(R.drawable.home);
  121.     MenuItem item2 = menu.add(0, 2, 0, "BACK");
  122.     item2.setIcon(R.drawable.arrowleft);
  123.     MenuItem item3 = menu.add(0, 3, 0, "F5");
  124.     item3.setIcon(R.drawable.s);
  125.     MenuItem item4 = menu.add(0, 4, 0, "CLEAR CACHE");
  126.     item4.setIcon(R.drawable.trash);
  127.     MenuItem item5 = menu.add(0, 5, 0, "VOID");
  128.     item5.setIcon(R.drawable.vote);
  129.     menu.add(0, 6, 0, "IMG ON");
  130.     menu.add(0, 7, 0, "IMG OFF");
  131.     return true;
  132.   }
  133.   
  134.   @Override
  135.   public boolean onOptionsItemSelected(MenuItem item)  {
  136.     switch (item.getItemId())
  137.     {
  138.       case 1:
  139.         wv.loadUrl("http://habrahabr.ru");
  140.         break;
  141.       case 2:
  142.         if (wv.canGoBack()) {
  143.           wv.goBack();
  144.         }
  145.         break;
  146.       case 3:
  147.         wv.loadUrl(LASTURL);
  148.         break;
  149.       case 4:
  150.         wv.clearCache(true);
  151.         break;
  152.       case 5:
  153.         Intent marketIntent2 = new Intent(Intent.ACTION_VIEW, Uri.parse(
  154.             "http://market.android.com/details?id=" + getPackageName()));
  155.           startActivity(marketIntent2);                
  156.         break;
  157.       case 6:
  158.         saveSettings(true);        
  159.         break;
  160.       case 7:
  161.         saveSettings(false);                
  162.         break;
  163.     }
  164.  
  165.     return true;
  166.   }
  167.   
  168.   private void saveSettings(Boolean val)
  169.   {
  170.     SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  171.     SharedPreferences.Editor editor = settings.edit();
  172.     editor.putBoolean("IMGMODE", val);
  173.     editor.commit();
  174.   }
  175. }
* This source code was highlighted with Source Code Highlighter.


UPD: Очень сильно переделал вёрстку.
За основу взял стиль разработанный almalexa.habrahabr.ru и существенно доработал его напильником под маленькое разрешение.
Получившийся стиль: userstyles.org/styles/46932/habr
На этом считаю разработку законченной. Клиент в маркете — обновлён.
Итого: на всё про всё ушли сутки.
image
Tags:
Hubs:
+115
Comments 66
Comments Comments 66

Articles