MójDroid.pl

Tworzymy czytnik kanałów RSS – Korzystamy z zewnętrznych bibliotek i pobieramy obrazy

2012-11-08
|
Damian P.

Pobieranie obrazów z sieci to jedna z tych rzeczy w Androidzie, która wymaga dużego nakładu pracy do poprawnego działania. O ile samo wyświetlenie grafiki nie jest problemem, tyle jej usunięcie, cachowanie czy zarządzanie w pamięci RAM jest już nie małym wyzwaniem. Na dodatek wszytko to musi odbywać się płynnie, a bateria użytkownika nie może iść na marne. Problem ten nie jest nowy i prawdopodobnie od początku istnienia systemu trapi programistów. Dlatego też Ci pomagają sobie - tworząc ogólnodostępne rozwiązania. Aby nie odkrywać koła na nowo, wykorzystamy jeden z takich pomysłów i dołączymy go do naszego programu. Zyskamy podwójnie, bo oszczędzimy czas jak i nauczymy się czegoś nowego - importowania zewnętrznych bibliotek to naszego własnego kodu.
Informacja: Nie radzisz sobie z kodem? Coś Ci nie wychodzi lub nie działa? Zgubiłeś się? W tym miejscu zobaczysz pełen kod aplikacji.
Ogólnodostępne źródła nie są złe. Takie projekty w większości to sam kod, więc dodatkowy plik w naszym programie nie kosztuje nas wiele. Sam import kłopotliwy również nie jest - kilka kliknięć i wszystko gotowe, a w niektórych przypadkach wystarczy zwykłe przeciągnięcie pobranego pliku. Ostatecznie musimy dodać do aplikacji linijkę lub dwie, po czym reszta zaczyna działać sama. Oczywiście, jeżeli ktoś z was czuje się na siłach to może napisać swoje (lepsze) rozwiązanie - w sieci znajdzie się również masa poradników i projektów z otwartym źródłem gotowym do nauki czy modyfikacji (ja swoje pobieranie obrazów napisałem sam korzystając wyłącznie z poradników na blogu Androida, polecam wam przy okazji to źródło). Przechodząc jednak do sedna problemu. "Gotowców" w internecie jest tak wiele, że trudno cokolwiek wybrać. Jedne rozwiązania są lepsze od innych, czasami jeden kod ma coś czego nie ma inny (a nam by się to przydało), a jeszcze kolejny jest bardzo lekki. Osobiście postawiłem na bardzo proste rozwiązanie z otwartym źródłem, ładnym kodem i przykładowym projektem - UrlImageViewHelper (to nazwa biblioteki).

1. Dodawanie zewnętrznych bibliotek do projektu

Nie jest to trudna rzecz. Jak pisałem wcześniej, importować nowe pliki do swojego kodu możemy na kilka sposobów, przy czym zasada ich działania różni się od otwartości samej biblioteki (czyli tego jak możemy nowy kod zaimportować).

Sposób pierwszy (nie ten będzie przez nas używany)

Jeżeli nasz wybrany projekt nie ma otwartego kodu i oferuje jedynie plik .JAR, to zbyt wielu rozwiązań nie posiadamy. Archiwum musimy przenieść do folderu libs (lub w starszych wersjach systemu lib) w projekcie i... Zostawić go tam. Pre-kompilator resztą się zajmie, przeszukując zmiany w programie, po czym w kolejnych krokach biblioteka zostanie dodana do projektu. Po tym możemy już aktualizować swoje źródło. Czasami jednak ten sposób z dziwnych powodów nie działa. Istnieje drugie rozwiązanie, które może nas uratować. Klikamy prawym przyciskiem myszy na nasz projekt i wybieramy jego ustawienia, gdzie następnie szukamy kategorii "Java Build Path", a w niej - libraries. Tam już poradzimy sobie bez najmniejszego kłopotu, bo po prawej stronie mamy obecne przyciski do wyboru rodzaju importowanych rozszerzeń (w naszym przypadku Add External JARs), które mówią same za siebie.

Sposób drugi (tego używamy)

Drugi sposób jest dla osób, które znalazły otwarty projekt (który jest oczywiście biblioteką) ale nie wiedzą jak go użyć bez kopiowania wszystkich klas do swojej paczki. Z pewnością będziemy potrzebowali plików nowego projektu (w naszym przypadku ten znajdziecie tutaj). Pobieramy je w całości (np. w zipie), po czym wypakowujemy w miejsce ze znaną nam lokalizacją. Kolejny krok to import do samego Eclipse. To nie jest trudne - wybieramy z paska ustawień File->Import, szukamy folderu, wybieramy projekt i po chwili dodawanie gotowe. Kolejny krok to wybranie ustawień dodanego zasobu - w nich tym razem wybieramy kategorię "Android", gdzie szukamy małego checkboxa z opisem "Is Library". Jeżeli pole jest odznaczone, zaznaczamy je. Przechodzimy do ustawień naszej aplikacji, ponownie szukamy sekcji "Android" i tym razem wybieramy "Add" obok listy połączonych projektów. Tam już z listy wybieramy nasz wyznaczony. Co dalej? Chwilę czekamy i gotowe.

2. Aktualizowanie naszego projektu do wymagań biblioteki

Pobieranie obrazów samo się nie rozpocznie. Musimy zdecydować kiedy akcja ma się zacząć, a kiedy ma zostać pominięta. Kolejny raz nie jest to trudne - jeżeli w pobieranych przez nas danych z RSS jest adres obrazu, wtedy kontynuujemy. W przeciwnym razie pomijamy kod. Problem tkwi w tym, że nasz parser nie obsługuje pobierania (adresów) obrazków, a same kanały RSS zazwyczaj i tak go nie dodają. Nie naprawimy tego, bo jest to od nas niezależne, ale zawsze możemy spróbować odszukać łącze to grafiki w opisie samej wiadomości. Tak więc musimy zrobić kilka rzeczy: nasz adapter listy musi sprawdzać, czy mamy dostępny link do obrazu, RSSItem musi mieć nowe pole do zapisywania hiperłączy, a RSSParser musi dodatkowo reagować na tag contentu, aby przeszukać wiadomość pod kątem tagów IMG. Zmiany publikuję niżej z opisem: AdapterListViewMain:
package pl.damianpiwowarski.parser;

import java.util.ArrayList;

import com.koushikdutta.urlimageviewhelper.UrlImageViewHelper;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class AdapterListViewMain extends BaseAdapter {

	// Podstawowe zmienne do wykorzystania
	private ArrayList data;
	private Context listContext;
	private LayoutInflater layoutInflater;

	public AdapterListViewMain(Context context, ArrayList data) {
		this.listContext = context;
		this.data = data;
		layoutInflater = LayoutInflater.from(listContext);
	}

	// Ta funkcja liczy nam ile elementów ma pojawić się na liście
	// Na testy dla tego przykładu damy ich 20
	public int getCount() {
		if (data != null) {
			return data.size();
		} else {
			return 0;
		}
	}

	// Pobranie danych dla jednego elementu
	// To zostawiamy puste
	public Object getItem(int position) {
		return position;
	}

	// Jak wyżej, tylko tutaj występuje identyfikator
	// To zostawiamy puste
	public long getItemId(int position) {
		return 0;
	}

	// Holder do cachowania elementów
	// Poprawia znacząco płynność
	private class CustomHolder {
		TextView tvTitle;
		TextView tvSite;
		ImageView ivImage;
	}

	// Pojedynczy element na liście
	public View getView(int position, View convertView, ViewGroup parent) {

		CustomHolder viewCache;
		RSSItem actualItem = data.get(position);

		// ConvertView - czy da się wykorzystać ponownie ostatnio usunięty
		// element na liście?
		if (convertView == null) {
			// Jest pusty, więc definiujemy podstawę
			convertView = layoutInflater.inflate(
					R.layout.item_view_main_listview, null);

			viewCache = new CustomHolder();

			// Cachujemy kolejne elementy
			viewCache.tvTitle = (TextView) convertView
					.findViewById(R.id.textView_title_item_view_main_listview);
			viewCache.tvSite = (TextView) convertView
					.findViewById(R.id.textView_site_item_view_main_listview);
			viewCache.ivImage = (ImageView) convertView
					.findViewById(R.id.imageView_photo_item_view_main_listview);

			convertView.setTag(viewCache);
		} else {
			viewCache = (CustomHolder) convertView.getTag();
		}

		// Wypełnienie!
		viewCache.tvTitle.setText(actualItem.getTitle());

		// Sprawdzamy, czy ten item posiada adres URL do obrazu
		if (actualItem.getImage() != null) {
			viewCache.ivImage.setVisibility(View.VISIBLE);
			Log.d("tag", actualItem.getImage());
			// Pobieramy obraz
			UrlImageViewHelper.setUrlDrawable(viewCache.ivImage, actualItem.getImage());
		} else {
			viewCache.ivImage.setVisibility(View.GONE);
		}

		return convertView;
	}

}
RSSParser:
package pl.damianpiwowarski.parser;

import java.lang.reflect.Method;
import java.util.ArrayList;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

public class RSSParser extends DefaultHandler {

	private ArrayList rssItems;
	private RSSItem parsingItem; // Aktualnie "przerabiana" wiadomość
	private StringBuilder stringBuilder; // Tutaj tworzone są tytuły, daty i tak dalej

	public RSSParser() {
		rssItems = new ArrayList(); // Tworzymy tablicę składającą się z elementów RSSItem
											// Tutaj będzie znajdować się więcej niż jeden element
	}

	@Override
	public void characters(char[] ch, int start, int length) {
		stringBuilder.append(ch, start, length); // To o czym mówiłem wyżej, odczytane znaki są łączone w całość
	}

	// Końcowa faza zapisu elementów
	@Override
	public void endElement(String uri, String localName, String qName) {

		if (parsingItem != null) {
			try {
				// To musimy zmienić dla dobra naszych funkcji
				if (qName.equals("content:encoded")) {
					qName = "content";
				}

				// Nasza sztuczka, która zrobi za nas przypisywanie danych :>

				// Nazwa funkcji-settera w RSSItem, np setTitle
				String functionName = "set"
						+ qName.substring(0, 1).toUpperCase() // Pierwszy znak musi być wielki
						+ qName.substring(1);
				// Szukamy metody
				Method function = RSSItem.class.getMethod(functionName,
						String.class);
				// I zaczynamy działanie na niej!

				// Wykrywamy czy jesteśmy w Content
				if (qName.equals("content")) {
					// Jesteśmy! Przekazujemy treść Content do setImage
					Method tempImageFunction = RSSItem.class.getMethod("setImage", String.class);
					tempImageFunction.invoke(parsingItem, stringBuilder.toString());
				}

				function.invoke(parsingItem, stringBuilder.toString());

			} catch (Exception e) {
				e.printStackTrace();
				// Obsługa błędu :(
			}
		}

	}

	// Tworzymy kolejny element RSSItem
	@Override
	public void startElement(String uri, String localName, String qName,
			Attributes attributes) {

		stringBuilder = new StringBuilder();

		if (qName.equals("item") && rssItems != null) {
			parsingItem = new RSSItem();
			rssItems.add(parsingItem); // Reszta danych za chwilę sama się dopisze
		}

	}

	public ArrayList getResult(){
		return rssItems; // Zwracamy elementy po fakcie
	}

}
RSSItem:
package pl.damianpiwowarski.parser;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;

public class RSSItem implements Parcelable {

	private String title;
	private String link;
	private String description;
	private String content;
	private String pubDate;
	private String image; // adres obrazka

	public RSSItem() {
		// Parcel będzie potrzebny tylko Androidowi, nie bezpośrednio nam
	}

	public RSSItem(Parcel parcel) {
		final Bundle data = parcel.readBundle();

		// odczytujemy parcel

		title = data.getString("title");
		link = data.getString("link");
		description = data.getString("description");
		content = data.getString("content");
		pubDate = data.getString("pubDate");
		image = data.getString("image");

	}

	@Override
	public void writeToParcel(Parcel dest, int flags) {
		final Bundle data = new Bundle();

		// zapisujemy parcel

		data.putString("title", title);
		data.putString("link", link);
		data.putString("description", description);
		data.putString("content", content);
		data.putSerializable("pubDate", pubDate);
		data.putString("image", image);

		dest.writeBundle(data);
	}

	public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
		public RSSItem createFromParcel(Parcel data) {
			return new RSSItem(data);
		}

		public RSSItem[] newArray(int size) {
			return new RSSItem[size];
		}
	};

	@Override
	public int describeContents() {
		return 0;
	}

	// "SETTERS AND GETTERS"
	// GETTERS

	public String getTitle() {
		return title;
	}

	public String getLink() {
		return link;
	}

	public String getDescription() {
		return description;
	}

	public String getContent() {
		return content;
	}

	public String getPubDate() {
		return pubDate;
	}

	public String getImage() {
		return image;
	}

	// I SETTERS

	public void setTitle(String title) {
		this.title = title;
	}

	public void setLink(String link) {
		this.link = link;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public void setPubDate(String pubDate) {
		this.pubDate = pubDate; // To będziemy musieli poprawić, bo data zwracana jest w takiej postaci jaka została wysłana
	}

	// Wyrażenie regularne szuka tagu IMG, a następnie odczytuje SRC z niego
	public void setImage(String descriptionWithImage) {
		// Wyrażenie
		Pattern p = Pattern.compile(".*]*src=\"([^\"]*)",
				Pattern.CASE_INSENSITIVE);
		// Szukamy!
		Matcher m = p.matcher(descriptionWithImage);

		// I jeżeli coś znajdziemy - pszypisujemy
		while (m.find()) {
			this.image = m.group(1);
		}
	}
}
Zmieniłem również layout w pliku z pojedynczym itemem na liście:
<!--?xml version="1.0" encoding="utf-8"?-->
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:background="#fff"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView_title_item_view_main_listview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:layout_marginTop="10dp"
        android:layout_toLeftOf="@+id/imageView_photo_item_view_main_listview"
        android:maxLines="2"
        android:text="Tytuł"
        android:textColor="#1e202c"
        android:textSize="18sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/textView_site_item_view_main_listview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@+id/textView_title_item_view_main_listview"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:layout_toLeftOf="@+id/imageView_photo_item_view_main_listview"
        android:lines="1"
        android:maxLines="1"
        android:paddingBottom="10dp"
        android:text="Strona"
        android:textColor="#c3c1c1"
        android:textSize="15sp" />

    <ImageView
        android:id="@+id/imageView_photo_item_view_main_listview"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentRight="true"
        android:adjustViewBounds="true"
        android:background="#000" android:scaleType="centerCrop" android:src="@drawable/image_example"/>

Cały kod programu jest oczywiście dostępny na GitHubie.