Статьи

Android Full App, часть 6: настраиваемое представление списка для представления данных

Это шестая часть серии «Android Full Tutorial» . Полное приложение предназначено, чтобы обеспечить легкий способ выполнять поиск фильмов / актеров через Интернет. В первой части серии ( «Основной интерфейс действий» ) мы создали проект Eclipse и настроили базовый интерфейс для основной деятельности приложения. Во второй части ( «Использование HTTP API» ) мы использовали клиентскую библиотеку Apache HTTP для использования внешнего HTTP API и интеграции возможностей поиска API в наше приложение. В третьей части ( «Анализ XML-ответа» ) мы увидели, как анализировать XML-ответ, используя встроенные в Android возможности синтаксического анализа XML. В четвертой части ( «Выполнение запроса API асинхронно из основного действия» ) мы связали вместе службы HTTP-ретривера и XML-парсера, чтобы выполнить запрос поиска API из основного действия нашего приложения. Запрос был выполнен асинхронно в фоновом потоке, чтобы избежать блокировки основного потока пользовательского интерфейса. В пятой части ( «Запуск новых действий с намерениями» ) мы увидели, как запустить новое действие и как перенести данные из одного действия в другое. В этой части мы собираемся создать собственное представление списка, чтобы обеспечить лучшее визуальное представление данных.

Представление, используемое для представления результатов поиска в нашем уроке, является довольно элементарным и простым. Если вы помните, результаты были переданы в ListActivity, который после рендеринга выглядел так:

Теперь мы собираемся оживить пользовательский интерфейс. Первый шаг — заменить ArrayAdapter, который мы использовали до этого момента, и внедрить собственный адаптер. Наш адаптер расширит класс ArrayAdapter и переопределит его метод getView для предоставления настраиваемого представления списка.

Помните, что класс модели Movie содержит различную информацию о соответствующем фильме, среди которых:

  • Рейтинг фильма
  • Дата выпуска
  • сертификация
  • язык
  • URL-адрес миниатюрного изображения

Мы собираемся создать пользовательский макет, который будет включать вышеупомянутые данные для каждого из фильмов, включенных в результаты поиска. Это изображение того, как будет выглядеть каждая строка списка:

(Примечание: наша реализация основана на великолепном примере, представленном здесь )

Файл XML, который описывает макет каждой строки, называется «movie_data_row.xml» и содержит следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?xml version="1.0" encoding="utf-8"?>
 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    android:padding="6dip">
     
    <ImageView
        android:id="@+id/movie_thumb_icon"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_marginRight="6dip"/>
         
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="0dip"
        android:layout_weight="1"
        android:layout_height="fill_parent">
         
        <TextView
            android:id="@+id/name_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
            android:textStyle="bold"
        />
         
        <TextView
            android:id="@+id/rating_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
         
        <TextView
            android:id="@+id/released_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
         
        <TextView
            android:id="@+id/certification_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
             
        <TextView
            android:id="@+id/language_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
           
        <TextView
            android:id="@+id/adult_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
                      
    </LinearLayout>
     
</LinearLayout>

Мы используем LinearLayout для базового макета, и внутри него мы включаем ImageView (который будет содержать миниатюру изображения) и другой LinearLayout, который является заполнителем для ряда TextView . Каждому элементу присваивается уникальный идентификатор, чтобы на него можно было впоследствии ссылаться из нашего адаптера.

Обратите внимание, что ArrayAdapter не может использовать метод setContentView, который обычно используется Activity, чтобы объявить макет, который будет использоваться. Чтобы получить макет XML во время выполнения, используйте сервис LayoutInflater . Этот класс используется для создания экземпляра XML-файла макета в соответствующих объектах View. Более конкретно, метод inflate используется для раздувания новой иерархии представления из указанного ресурса xml. После того, как мы взяли ссылку на базовый вид, мы можем использовать его как обычно и изменять его внутренние виджеты, то есть предоставлять текст в TextView и загружать изображение в ImageView . Вот код для нашего адаптера:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.javacodegeeks.android.apps.moviesearchapp.ui;
 
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.LinkedHashMap;
 
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
 
import com.javacodegeeks.android.apps.moviesearchapp.R;
import com.javacodegeeks.android.apps.moviesearchapp.io.FlushedInputStream;
import com.javacodegeeks.android.apps.moviesearchapp.model.Movie;
import com.javacodegeeks.android.apps.moviesearchapp.services.HttpRetriever;
 
public class MoviesAdapter extends ArrayAdapter<Movie> {
     
    private HttpRetriever httpRetriever = new HttpRetriever();
     
    private ArrayList<Movie> movieDataItems;
     
    private Activity context;
     
    public MoviesAdapter(Activity context, int textViewResourceId, ArrayList<Movie> movieDataItems) {
        super(context, textViewResourceId, movieDataItems);
        this.context = context;
        this.movieDataItems = movieDataItems;
    }
     
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
   
        View view = convertView;
        if (view == null) {
            LayoutInflater vi = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            view = vi.inflate(R.layout.movie_data_row, null);
        }
         
        Movie movie = movieDataItems.get(position);
         
        if (movie != null) {
             
            // name
            TextView nameTextView = (TextView) view.findViewById(R.id.name_text_view);
            nameTextView.setText(movie.name);
             
            // rating
            TextView ratingTextView = (TextView) view.findViewById(R.id.rating_text_view);
            ratingTextView.setText("Rating: " + movie.rating);
             
            // released
            TextView releasedTextView = (TextView) view.findViewById(R.id.released_text_view);
            releasedTextView.setText("Release Date: " + movie.released);
             
            // certification
            TextView certificationTextView = (TextView) view.findViewById(R.id.certification_text_view);
            certificationTextView.setText("Certification: " + movie.certification);
             
            // language
            TextView languageTextView = (TextView) view.findViewById(R.id.language_text_view);
            languageTextView.setText("Language: " + movie.language);
             
            // thumb image
            ImageView imageView = (ImageView) view.findViewById(R.id.movie_thumb_icon);
            String url = movie.retrieveThumbnail();
             
            if (url!=null) {
                Bitmap bitmap = fetchBitmapFromCache(url);
                if (bitmap==null) {               
                    new BitmapDownloaderTask(imageView).execute(url);
                }
                else {
                    imageView.setImageBitmap(bitmap);
                }
            }
            else {
                imageView.setImageBitmap(null);
            }
             
        }
         
        return view;
         
    }
     
    private LinkedHashMap<String, Bitmap> bitmapCache = new LinkedHashMap<String, Bitmap>();
     
    private void addBitmapToCache(String url, Bitmap bitmap) {
        if (bitmap != null) {
            synchronized (bitmapCache) {
                bitmapCache.put(url, bitmap);
            }
        }
    }
     
    private Bitmap fetchBitmapFromCache(String url) {
         
        synchronized (bitmapCache) {
            final Bitmap bitmap = bitmapCache.get(url);
            if (bitmap != null) {
                // Bitmap found in cache
                // Move element to first position, so that it is removed last
                bitmapCache.remove(url);
                bitmapCache.put(url, bitmap);
                return bitmap;
            }
        }
 
        return null;
         
    }
     
    private class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
         
        private String url;
        private final WeakReference<ImageView> imageViewReference;
 
        public BitmapDownloaderTask(ImageView imageView) {
            imageViewReference = new WeakReference<ImageView>(imageView);
        }
         
        @Override
        protected Bitmap doInBackground(String... params) {
            url = params[0];
            InputStream is = httpRetriever.retrieveStream(url);
            if (is==null) {
                  return null;
            }
            return BitmapFactory.decodeStream(new FlushedInputStream(is));
        }
         
        @Override
        protected void onPostExecute(Bitmap bitmap) {           
            if (isCancelled()) {
                bitmap = null;
            }
             
            addBitmapToCache(url, bitmap);
 
            if (imageViewReference != null) {
                ImageView imageView = imageViewReference.get();
                if (imageView != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
    }
     
}

Внутри нашего метода getView мы сначала раздуваем файл макета XML и получаем ссылку на описанный вид . Затем мы берем ссылку на каждый из виджетов представлений, используя метод findViewById . Для каждого TextView мы предоставляем соответствующий текст, в то время как для каждого ImageView мы предоставляем растровое изображение, которое содержит эскиз изображения.

На этом этапе используется очень простой механизм кэширования, чтобы избежать повторной загрузки одного и того же изображения снова и снова. Не забывайте, что метод getView будет вызываться несколько раз, когда пользователь играет с интерфейсом, поэтому мы определенно не хотим выполнять HTTP-запросы для одного и того же изображения. По этой причине используется карта, содержащая связь URL-битмап. Если изображение не найдено в кеше, запускается фоновая задача, чтобы получить изображение (и сохранить его в кеше для последующих вызовов). Фоновая задача называется «BitmapDownloaderTask» и расширяет класс AsyncTask (пожалуйста, обратитесь к одному из наших предыдущих руководств, если вы хотите узнать больше о том, как использовать AsyncTask ).

Также обратите внимание, что внутри каждой задачи на экземпляр ImageView ссылаются через WeakReference . Это сделано из соображений производительности и, более конкретно, для того, чтобы сборщик мусора виртуальной машины мог собирать любые изображения ImageView, которые могут принадлежать уничтоженной деятельности. Другими словами, мы не хотим, чтобы деятельность содержала сильные ссылки на свои ImageView, чтобы их можно было легко очистить. Посетите официальный блог разработчика Android для получения дополнительной информации об этом.

Что касается активности в списке, то здесь тоже нужно внести некоторые изменения. Самым большим изменением является то, что оригинальный ArrayAdapter был заменен нашим. Мы заполняем адаптер содержимым объектов результатов поиска, а затем вызываем для него метод notifyDataSetChanged , чтобы уведомить прикрепленное представление о том, что базовые данные были изменены, и он должен обновить себя. Это код для новой реализации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.javacodegeeks.android.apps.moviesearchapp;
 
import java.util.ArrayList;
 
import android.app.ListActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
import android.widget.Toast;
 
import com.javacodegeeks.android.apps.moviesearchapp.model.Movie;
import com.javacodegeeks.android.apps.moviesearchapp.ui.MoviesAdapter;
 
public class MoviesListActivity extends ListActivity {
     
    private static final String IMDB_BASE_URL = "http://m.imdb.com/title/";
     
    private ArrayList<Movie> moviesList = new ArrayList<Movie>();
    private MoviesAdapter moviesAdapter;
     
    @SuppressWarnings("unchecked")
    @Override
    public void onCreate(Bundle savedInstanceState) {
         
        super.onCreate(savedInstanceState);
        setContentView(R.layout.movies_layout);
 
        moviesAdapter = new MoviesAdapter(this, R.layout.movie_data_row, moviesList);
        moviesList = (ArrayList<Movie>) getIntent().getSerializableExtra("movies");
         
        setListAdapter(moviesAdapter);
         
        if (moviesList!=null && !moviesList.isEmpty()) {
             
            moviesAdapter.notifyDataSetChanged();
            moviesAdapter.clear();
            for (int i = 0; i < moviesList.size(); i++) {
                moviesAdapter.add(moviesList.get(i));
            }
        }
         
        moviesAdapter.notifyDataSetChanged();
         
    }
     
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
         
        super.onListItemClick(l, v, position, id);
        Movie movie = moviesAdapter.getItem(position);
         
        String imdbId = movie.imdbId;
        if (imdbId==null || imdbId.length()==0) {
            longToast(getString(R.string.no_imdb_id_found));
            return;
        }
         
        String imdbUrl = IMDB_BASE_URL + movie.imdbId;
         
        Intent imdbIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(imdbUrl));               
        startActivity(imdbIntent);
         
    }
     
    public void longToast(CharSequence message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }
     
}

Запустите приложение и введите поисковый запрос. Когда активируется «MoviesListActivity», вы сначала увидите список с фильмами и соответствующей информацией. Постепенно различные изображения начнут появляться по мере их загрузки! Не забывайте, что эта операция в конце концов происходит в фоновом режиме. Обратите внимание, что некоторые фильмы, особенно старые, не имеют соответствующих миниатюр. Посмотрите на следующие фотографии:

Это оно! Вы можете скачать здесь созданный проект Eclipse.

Статьи по Теме :