Статьи

Создание приложения погоды для Марса с использованием залпа

Конечный продукт
Что вы будете создавать

В этом уроке я покажу вам возможный вариант использования того, что мы узнали в предыдущей статье о волейболе. Мы создадим метеорологическое приложение для Марса, используя информацию, собранную ровером Curiosity, которая доступна для всех НАСА через API {MAAS} .

Сначала мы настроим проект в Android Studio и разработаем пользовательский интерфейс. Затем мы структурируем ядро ​​приложения, используя Volley. Поскольку каждое красивое приложение имеет несколько изображений, я покажу вам, как получить случайное приложение с помощью API Flickr. Мы загрузим картинку с Volley, в основном из-за ее отличной системы кеширования. Наконец, мы добавим некоторые интересные детали, чтобы приложение выглядело великолепно.

Сначала создайте новый проект в Android Studio. Поскольку Volley обладает обратной совместимостью, вы можете выбрать любой уровень API, который предпочитаете. Я выбрал API 21, но с вами все будет в порядке, если уровень API равен 8 (Froyo) или выше.

Наше приложение имеет одно простое действие. Вы можете назвать его MainActivity.java , как предлагает Android Studio. Откройте редактор макетов и дважды щелкните файл activity_main.xml .

Поскольку нам хотелось бы иметь около 70% экрана, посвященного изображению, а остальную часть — информации о погоде, нам необходимо использовать атрибут XML layout_weight . Конечно, мы можем использовать и абсолютные значения, но это не будет так же. К сожалению, в мире Android есть дисплеи, которые не являются однородными, и указание абсолютного значения высоты изображения может привести к соотношению 90-10 на очень маленьких устройствах и 70-30, или даже 60-40, на больших устройствах. layout_weight   Атрибут это то, что вам нужно для решения этой проблемы.

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
<LinearLayout xmlns:android=»http://schemas.android.com/apk/res/android»
    android:layout_width=»match_parent»
    android:layout_height=»match_parent»
    android:orientation=»vertical»>
 
    <RelativeLayout
        android:layout_width=»match_parent»
        android:layout_height=»0dp»
        android:layout_weight=»0.68″
        android:background=»#FF5722″>
 
        <!— image —>
 
    </RelativeLayout>
 
    <RelativeLayout
        android:layout_weight=»0.33″
        android:layout_height=»0dp»
        android:layout_width=»match_parent»
        android:paddingTop=»@dimen/activity_horizontal_margin»
        android:background=»#212121″>
 
        <!— TextViews —>
 
    </RelativeLayout>
 
</LinearLayout>

Внутри первого потомка добавьте ImageView :

1
2
3
4
5
<ImageView
   android:id=»@+id/main_bg»
   android:layout_width=»match_parent»
   android:layout_height=»match_parent»
   android:scaleType=»centerCrop»/>

Во втором RelativeLayout мы добавляем список элементов TextView . Два из них представляют собой изображения, на которых показана средняя температура и непрозрачность атмосферы. Третий — метка ошибки.

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
<TextView
    android:id=»@+id/error»
    android:layout_centerInParent=»true»
    android:visibility=»gone»
    android:layout_height=»wrap_content»
    android:layout_width=»match_parent»
    android:textSize=»20sp»
    android:textColor=»#FF5722″
    android:layout_margin=»@dimen/activity_horizontal_margin»
    android:gravity=»center»
    android:text=»I’m sorry.\nI wasn’t able to retrieve real time data.»/>
 
<TextView
    android:id=»@+id/degrees»
    android:layout_height=»wrap_content»
    android:layout_width=»wrap_content»
    android:layout_centerHorizontal=»true»
    android:textSize=»90sp»
    android:textColor=»#FF5722″
    android:text=»-36°»/>
 
<TextView
    android:id=»@+id/weather»
    android:layout_width=»wrap_content»
    android:layout_height=»wrap_content»
    android:layout_centerHorizontal=»true»
    android:layout_below=»@id/degrees»
    android:textSize=»30sp»
    android:gravity=»center»
    android:textColor=»#FF5722″
    android:text=»Sunny»/>

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

Есть еще две вещи, о которых нужно позаботиться, прежде чем начинать копаться в ядре приложения. Измените унаследованную тему приложения на android:Theme.Material.Light.NoActionBar . Это означает, что нам не нужно скрывать панель действий во время выполнения.

1
<style name=»AppTheme» parent=»android:Theme.Material.Light.NoActionBar»/>

Наконец, добавьте интернет-разрешение в манифест проекта.

1
<uses-permission android:name=»android.permission.INTERNET» />

Как мы уже говорили в предыдущей статье , самый простой и надежный способ использовать Volley — это импортировать библиотеку как новый модуль. Загрузите исходный код библиотеки, импортируйте его через « Файл» > « Создать» > « Модуль» и скажите компилятору в файле build.gradle проекта, чтобы он был включен в проект.

1
compile project(«:volley»)

Как я уже указывал в предыдущей статье , если вам нужно запустить несколько запросов, лучше использовать общую очередь запросов. Вам следует избегать создания очереди запросов каждый раз, когда вы планируете запрос, вызывая Volley.newRequestQueue , потому что вы не хотите заканчиваться утечками памяти и другими нежелательными проблемами.

Для этого сначала нужно создать класс, используя шаблон синглтона. На класс ссылаются с помощью статической, глобально видимой переменной, которая затем обрабатывает объект RequestQueue . Таким образом, вы получите единый RequestQueue для приложения. Затем, расширяя класс Application , вы должны указать операционной системе сгенерировать этот объект при запуске приложения, даже до создания первого действия.

Поскольку мы находимся в среде Android, мы немного модифицируем общую структуру синглтона. Класс должен создать новый экземпляр самого себя в методе Application.onCreate не в общем методе getInstance когда он равен null .

Для этого создайте новый класс и назовите его MarsWeather.java . Затем расширьте класс Application Android, переопределите метод onCreate и инициализируйте объект RequestQueue статического экземпляра.

В одноэлементном классе мы создаем объект класса, используя public и synchronized функцию getInstance . Внутри этого метода мы возвращаем переменную mInstance . Метод onCreate вызывается при запуске приложения, поэтому переменная mInstance уже будет установлена ​​при первом getInstance метода getInstance .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class MarsWeather extends Application {
 
    private RequestQueue mRequestQueue;
    private static MarsWeather mInstance;
 
    @Override
    public void onCreate() {
        super.onCreate();
        mInstance = this;
        mRequestQueue = Volley.newRequestQueue(getApplicationContext());
    }
 
    public static synchronized MarsWeather getInstance() {
        return mInstance;
    }
     
}

Затем скажите в файле AndroidManifest.xml, что вы хотите, чтобы MarsWeather загружался при запуске приложения. В <application> добавьте name атрибута следующим образом:

1
android:name=».MarsWeather»

Вот и все. Экземпляр класса Application создается даже до MainActivity . Наряду со всеми другими стандартными операциями onCreate генерирует экземпляр RequestQueue .

Нам нужно реализовать три других метода для завершения вспомогательного класса. Первый метод заменяет Volley.newRequestQueue , который я назову getRequestQueue . Нам также нужен метод для добавления запроса в очередь add и метод, который отвечает за отмену запросов, cancel . Следующий блок кода показывает, как выглядит реализация.

01
02
03
04
05
06
07
08
09
10
11
12
public RequestQueue getRequestQueue() {
    return mRequestQueue;
}
 
public <T> void add(Request<T> req) {
    req.setTag(TAG);
    getRequestQueue().add(req);
}
 
public void cancel() {
    mRequestQueue.cancelAll(TAG);
}

TAG — это общий токен, который вы используете для идентификации запроса. В данном конкретном случае это может быть все, что вы хотите:

1
public static final String TAG = MarsWeather.class.getName();

Как вы уже знаете, Volley предоставляет три стандартных типа запросов: StringRequest , ImageRequest и JsonRequest . Наше приложение будет использовать последнее для получения данных о погоде и получения списка случайных изображений.

По умолчанию Volley устанавливает приоритет запроса на NORMAL . Обычно это было бы хорошо, но в нашем приложении у нас есть два совершенно разных запроса, и поэтому мы должны иметь другой приоритет в очереди. Выборка данных о погоде должна иметь более высокий приоритет, чем выборка URL-адреса случайного изображения.

По этой причине нам нужно настроить класс JsonRequest . Создайте новый класс с именем CustomJsonRequest.java и убедитесь, что он расширяет JsonObjectRequest . Затем переопределите метод getPriority как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class CustomJsonRequest extends JsonObjectRequest {
 
    public CustomJsonRequest(int method, String url, JSONObject jsonRequest,
                             Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
        super(method, url, jsonRequest, listener, errorListener);
    }
 
    private Priority mPriority;
 
    public void setPriority(Priority priority) {
        mPriority = priority;
    }
 
    @Override
    public Priority getPriority() {
        return mPriority == null ?
    }
 
}

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

1
http://marsweather.ingenology.com/v1/latest/

API-интерфейсы доступны для просмотра, поэтому откройте ссылку, чтобы проверить полученный JSON. JSON содержит простой объект, result , который включает в себя ряд строк, начиная от температуры до направления ветра и времени заката.

Начните с объявления следующих переменных в классе MainActivity :

1
2
3
TextView mTxtDegrees, mTxtWeather, mTxtError;
MarsWeather helper = MarsWeather.getInstance();
final static string RECENT_API_ENDPOINT = «http://marsweather.ingenology.com/v1/latest/»;

Вы можете вызвать MarsWeather.getInstance вне onCreate . Поскольку класс уже будет инициализирован, вам не нужно ждать, onStart метод onStart вызовет его. Конечно, вы должны установить ссылки представлений пользовательского интерфейса в   метод onCreate .

1
2
3
mTxtDegrees = (TextView) findViewById(R.id.degrees);
mTxtWeather = (TextView) findViewById(R.id.weather);
mTxtError = (TextView) findViewById(R.id.error);

После этого пришло время реализовать метод loadWeatherData . Мы создаем пользовательский запрос залпа и устанавливаем приоритет HIGH . Затем мы вызываем метод add помощника, чтобы добавить его в очередь запросов. Важно отметить прослушиватель результатов, поскольку он влияет на пользовательский интерфейс.

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
private void loadWeatherData() {
    CustomJsonRequest request = new CustomJsonRequest
        (Request.Method.GET, RECENT_API_ENDPOINT, null, new Response.Listener<JSONObject>() {
 
            @Override
            public void onResponse(JSONObject response) {
                try {
 
                    String minTemp, maxTemp, atmo;
                    int avgTemp;
 
                    response = response.getJSONObject(«report»);
 
                    minTemp = response.getString(«min_temp»);
                    maxTemp = response.getString(«max_temp»);
 
                    avgTemp = (Integer.parseInt(minTemp)+Integer.parseInt(maxTemp))/2;
 
                    atmo = response.getString(«atmo_opacity»);
 
 
                    mTxtDegrees.setText(avgTemp+»°»);
                    mTxtWeather.setText(atmo);
 
                } catch (Exception e) {
                    txtError(e);
                }
 
            }
        }, new Response.ErrorListener() {
 
            @Override
            public void onErrorResponse(VolleyError error) {
                txtError(error);
            }
        });
 
    request.setPriority(Request.Priority.HIGH);
    helper.add(request);
 
}

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

1
2
3
4
private void txtError(Exception e) {
    mTxtError.setVisibility(View.VISIBLE);
    e.printStackTrace();
}

Теперь нам нужно только вызвать loadWeatherData в onCreate и все готово. Приложение теперь готово показать погоду Марса.

Теперь, когда у вас есть готовое ядро ​​приложения, мы можем сосредоточиться на том, чтобы сделать приложение визуально более привлекательным. Мы собираемся сделать это, выбрав случайное изображение Марса и отобразив его пользователю.

Вам понадобится ключ API Flickr, чтобы получить случайный список контекстуализированных изображений. Конечная точка изображения следующая:

1
2
3
https://api.flickr.com/services/rest/?format=json&nojsoncallback=1&
sort=random&method=flickr.photos.search&tags=mars,planet,rover&tag_mode=all&
api_key=[YOUR_KEY]

Как видите, запрос довольно прост. Вы говорите Flickr дать вам результаты в формате JSON ( format=json ), но мы не указываем обратный вызов JSON ( nojsoncallback=1 ). Вы ищете изображение ( method=flickr.photos.search ), и интересующие вас теги связаны с Марсом ( tags=mars,planet,rover ). Посмотрите документацию для получения дополнительной информации о формате URL запроса.

Начните с объявления следующих переменных:

1
2
3
4
final static String
   FLICKR_API_KEY = «[INSERT HERE YOUR API KEY]»,
   IMAGES_API_ENDPOINT = «https://api.flickr.com/services/rest/?format=json&nojsoncallback=1&sort=random&method=flickr.photos.search&» +
           «tags=mars,planet,rover&tag_mode=all&api_key=»;

Затем реализуем метод searchRandomImage :

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
private void searchRandomImage() throws Exception {
    if (FLICKR_API_KEY.equals(«»))
        throw new Exception(«You didn’t provide a working Flickr API!»);
 
    CustomJsonRequest request = new CustomJsonRequest
        (Request.Method.GET, IMAGES_API_ENDPOINT+ FLICKR_API_KEY, null, new Response.Listener<JSONObject>() {
 
            @Override
            public void onResponse(JSONObject response) {
                try {
                    JSONArray images = response.getJSONObject(«photos»).getJSONArray(«photo»);
                    int index = new Random().nextInt(images.length());
 
                    JSONObject imageItem = images.getJSONObject(index);
 
                    String imageUrl = «http://farm» + imageItem.getString(«farm») +
                            «.static.flickr.com/» + imageItem.getString(«server») + «/» +
                            imageItem.getString(«id») + «_» + imageItem.getString(«secret») + «_» + «c.jpg»;
 
                    // TODO: do something with *imageUrl*
 
                } catch (Exception e) { imageError(e);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                imageError(error);
            }
        });
    request.setPriority(Request.Priority.LOW);
    helper.add(request);
}

Как видите, Flickr отправляет обратно JSONArray содержащий изображения. Метод, который я написал для получения случайного изображения, генерирует случайное число от нуля до размера массива. Он берет элемент, соответствующий этому индексу, из массива результатов и создает URL для изображения, следуя этим рекомендациям .

Как и прежде, нам нужен метод для обработки ошибок:

1
2
3
4
5
6
int mainColor = Color.parseColor(«#FF5722»);
 
private void imageError(Exception e) {
    mImageView.setBackgroundColor(mainColor);
    e.printStackTrace();
}

Наконец, вызовите searchRandomImage в методе onCreate и не забудьте перехватить любые исключения.

Теперь, когда у нас есть URL для загрузки, мы можем показать картинку. Вы уже узнали, как это сделать, в предыдущей статье .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
private void loadImg(String imageUrl) {
    // Retrieves an image specified by the URL, and displays it in the UI
    ImageRequest request = new ImageRequest(imageUrl,
            new Response.Listener<Bitmap>() {
                @Override
                public void onResponse(Bitmap bitmap) {
                    mImageView.setImageBitmap(bitmap);
                }
            }, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.ARGB_8888,
            new Response.ErrorListener() {
                public void onErrorResponse(VolleyError error) {
                    imageError(error);
                }
            });
 
    // we don’t need to set the priority here;
    // ImageRequest already comes in with
    // priority set to LOW, that is exactly what we need.
    helper.add(request);
}

В методе onResponse который мы написали на предыдущем шаге, мы наконец-то смогли обработать результат.

1
loadImg(imageUrl);

Возможно, вы уже заметили, что мы обходим систему кэширования Volley, выбирая случайное изображение при каждом запуске приложения. Нам нужно найти способ показать одно и то же изображение в определенный день.

Самый простой способ добиться этого — использовать SharedPreferences Android. Начните с объявления переменных, которые нам понадобятся для этого.

1
2
3
4
5
6
SharedPreferences mSharedPref;
 
int today = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
 
final static String SHARED_PREFS_IMG_KEY = «img»,
SHARED_PREFS_DAY_KEY = «day»;

Затем в методе onCreate перед вызовом searchRandomImage инициализируйте mSharedPref .

1
mSharedPref = getPreferences(Context.MODE_PRIVATE);

Идея состоит в том, чтобы хранить текущий день каждый раз, когда мы получаем новую случайную картинку. Конечно, мы храним URL изображения вместе с днем. Когда приложение запускается, мы проверяем, есть ли у нас уже запись в SharedPreferences на текущий день. Если у нас есть совпадение, мы используем сохраненный URL. В противном случае мы выбираем случайное изображение и сохраняем его URL в SharedPreferences .

В searchRandomImage после определения imageUrl добавьте следующие строки кода:

1
2
3
4
5
6
7
8
9
// right after *String imageUrl = …. *
 
// store the pict of the day
SharedPreferences.Editor editor = mSharedPref.edit();
editor.putInt(SHARED_PREFS_DAY_KEY, today);
editor.putString(SHARED_PREFS_IMG_KEY, imageUrl);
editor.commit();
     
// and then there’s *loadImage(imageUrl);*

Метод onCreate после определения в mSharedPref теперь становится:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
if (mSharedPref.getInt(SHARED_PREFS_DAY_KEY, 0) != today) {
    // search and load a random mars pict
    try {
        searchRandomImage();
    } catch (Exception e) {
        // please remember to set your own Flickr API!
        // otherwise I won’t be able to show
        // a random Mars picture
        imageError(e);
    }
} else {
    // we already have a pict of the day: let’s load it
    loadImg(mSharedPref.getString(SHARED_PREFS_IMG_KEY, «»));
}
loadWeatherData();

Вот и все. Ваша заявка   готов. Не стесняйтесь загружать исходные файлы этого руководства на GitHub, чтобы увидеть завершенный проект. Посмотрите на проект, если вы столкнулись с проблемами.

Шрифт, используемый в пользовательском интерфейсе, часто определяет внешний вид приложения. Давайте начнем с изменения шрифта Roboto по умолчанию более привлекательным шрифтом, таким как Lato light .

Создайте новую папку с именем шрифты в папке ресурсов . Если вы не можете найти папку ресурсов , вы должны создать ее на том же уровне, что и папка Java . Структура папок должна выглядеть примерно так: app \ src \ main \ assets \ fonts .

Скопируйте файл Lato-light.ttf в папку шрифтов . В методе onCreate вам необходимо переопределить гарнитуру по умолчанию для представлений, в которых вы хотите использовать новый шрифт.

1
2
mTxtDegrees.setTypeface(Typeface.createFromAsset(getAssets(), «fonts/Lato-light.ttf»));
mTxtWeather.setTypeface(Typeface.createFromAsset(getAssets(), «fonts/Lato-light.ttf»));

Следуя рекомендациям по Android Material Design, мы можем сделать строку состояния прозрачной. Таким образом, фон будет частично виден через строку состояния.

Вы можете добиться этого, внеся небольшое изменение в тему приложения. Отредактируйте файл v21 \ style.xml проекта следующим образом:

1
2
3
4
5
<resources>
    <style name=»AppTheme» parent=»android:Theme.Material.Light.NoActionBar»>
        <item name=»android:windowTranslucentStatus»>true</item>
    </style>
</resources>

Убедитесь, что AndroidManifest.xml уже настроен на использование темы:

1
2
3
4
5
6
<application
       android:name=».MarsWeather»
       android:allowBackup=»true»
       android:icon=»@mipmap/ic_launcher»
       android:label=»@string/app_name»
       android:theme=»@style/AppTheme» >

Мы проделали долгий путь. В первой статье мы начали говорить о Volley и его приложениях. В этом уроке мы рассмотрели практический способ реализации понятий, которые мы изучили, создав приложение погоды для Марса. Теперь вы должны хорошо понимать библиотеку Volley, как она работает и для чего вы можете ее использовать.