Евгений Колесников 4 år sedan
förälder
incheckning
b1224d6262
5 ändrade filer med 302 tillägg och 18 borttagningar
  1. 8 2
      api.http
  2. 211 15
      articles/weather2.md
  3. BIN
      img/04032.png
  4. 1 1
      readme.md
  5. 82 0
      shpora/WeatherAdapter.kt

+ 8 - 2
api.http

@@ -1,6 +1,12 @@
 @lat=56.638372
 @lon=47.892991
 @token=d4c9eea0d00fb43230b479793d6aa78f
+### Запрос текущей погоды
+GET https://api.openweathermap.org/data/2.5/weather?lat={{lat}}&lon={{lon}}&appid={{token}}&lang=ru&units=metric
+
+### Запрос погоды за 5 дней https://openweathermap.org/forecast5
+GET https://api.openweathermap.org/data/2.5/forecast?q=Йошкар-Ола&appid={{token}}&lang=ru&units=metric
+
+### Запрос погоды за 5 дней https://openweathermap.org/forecast5
+GET https://api.openweathermap.org/data/2.5/forecast?lat={{lat}}&lon={{lon}}&appid={{token}}&lang=ru&units=metric
 
-# Запрос текущей погоды
-GET https://api.openweathermap.org/data/2.5/weather?lat={{lat}}&lon={{lon}}&units=metric&appid={{token}}&lang=ru

+ 211 - 15
articles/weather2.md

@@ -23,20 +23,20 @@
 
 * первым элементом кладем **ImageView** с картинкой, задав ему режим растягивания на весь экран `android:scaleType="fitXY"` и Z-индекс `android:elevation="999dp"`
 
-```xml
-<ImageView
-    android:scaleType="fitXY"
-    android:elevation="999dp"
-
-    android:id="@+id/splash"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    app:layout_constraintBottom_toBottomOf="parent"
-    app:layout_constraintEnd_toEndOf="parent"
-    app:layout_constraintStart_toStartOf="parent"
-    app:layout_constraintTop_toTopOf="parent"
-    app:srcCompat="@drawable/splash" />
-```
+    ```xml
+    <ImageView
+        android:scaleType="fitXY"
+        android:elevation="999dp"
+
+        android:id="@+id/splash"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/splash" />
+    ```
 
 * в коде главного окна в момент, когда нужно убрать заставку меняем Z-индекс заставки
 
@@ -114,6 +114,7 @@
 
     * задаем обработчик клика по элементу списка
 
+        ```kt
         cityListView.setOnItemClickListener { parent, view, position, id ->
             // получаем название города
             val cityName = cityNames[position]
@@ -142,4 +143,199 @@
     }
     ```
 
-    >Метод *onActivityResult* гугл объявил устаревшим (deprecated), и в IDE он помечается как ошибка - надо в контекстном меню "More action..." выбрать "Supress: add...". Перед методом будет добавлена аннотация `@SuppressLint("MissingSuperCall")`.
+    >Метод *onActivityResult* гугл объявил устаревшим (deprecated), и в IDE он помечается как ошибка - надо в контекстном меню "More action..." выбрать "Supress: add...". Перед методом будет добавлена аннотация `@SuppressLint("MissingSuperCall")`.
+
+## Веделение лямбда-выражения в отдельную переменную
+
+Для обработки результатов мы пользовались такой конструкцией - лямбда выражение передавали сразу в метод *requestGET*. 
+
+```kt
+HTTP.requestGET(url) {result, error ->
+    if(result != null) {
+        val json = JSONObject(result)
+        val wheather = json.getJSONArray("weather")
+        val icoName = wheather.getJSONObject(0).getString("icon")
+        val temp = json.getJSONObject("main").getDouble("temp")
+
+        runOnUiThread {
+            textView.text = json.getString("name")
+        }
+        ...
+    }
+}
+```
+
+Но теперь тот же код будет использоваться для получения погоды по городу. Поэтому имеет смысл вынести код в отдельную переменную и использовать её в обоих вызовах:
+
+```kt
+// callback - свойство класса, объявляется в теле класса
+private val callback: (result: String?, error: String) -> Unit = {result, error ->
+    if(result != null) {
+        val json = JSONObject(result)
+        val wheather = json.getJSONArray("weather")
+        val icoName = wheather.getJSONObject(0).getString("icon")
+        val temp = json.getJSONObject("main").getDouble("temp")
+
+        runOnUiThread {
+            textView.text = json.getString("name")
+        }
+        ...
+    }
+}
+
+...
+
+// при запросе погоды используем переменную, объявленную выше
+HTTP.requestGET(url, callback) 
+```
+
+## Получение и разбор массива данных. Вывод списка на экран.
+
+Для начала определимся со структурой формы:
+
+Всё окно разбито по вертикали на три блока (указаны стрелками на рисунке ниже)
+
+* первый блок - детальная информация о выбранной погоде
+* второй блок - горизонтальный список кратких данных о погоде
+* третий блок - панель с кнопками (пока у нас там только "Поиск города", но может ещё что-то придумаем)
+
+![](../img/04032.png)
+
+### Класс погода
+
+Для хранения массива полученных данных нам нужно описать структуру элемента списка. Для этого в котлине есть **data class** - класс, который содержит только свойства.
+
+Выглядит он примерно так (каждый класс жедательно заворачивать в отдельный файл)
+
+```kt
+data class Weather (
+    val dt: Int,
+    val mainTemp: Double,
+    val mainHumidity: Int,
+    val weatherIcon: String,
+    val weatherDescription: String,
+    val windSpeed: Double,
+    val windDeg: Int,
+    val dtTxt: String
+    )
+```
+
+### Заполнение массива данных о погоде
+
+С моим бесплатным аккаунтом на **openweathermap** кроме текущих данных можно запросить только список за 5 дней:
+
+```
+### Запрос погоды за 5 дней https://openweathermap.org/forecast5
+GET https://api.openweathermap.org/data/2.5/forecast?lat={{lat}}&lon={{lon}}&appid={{token}}&lang=ru&units=metric
+```
+
+1. Объявим в классе главного окна массив данных о погоде
+
+    ```kt
+    private val weatherList = ArrayList<Weather>()
+    ```
+
+2. Получаем и заполняем массив 
+
+    ```kt
+    val url = "https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${token}&lang=ru&units=metric"
+    HTTP.requestGET(url) {result, error ->
+        if(result != null) {
+            // перед заполнением очищаем список
+            weatherList.clear()
+
+            val json = JSONObject(result)
+            val list = json.getJSONArray("list")
+
+            // перебираем json массив
+            for(i in 0 until list.length()){
+                val item = list.getJSONObject(i)
+                val weather = item.getJSONArray("weather").getJSONObject(0)
+
+                // добавляем в список новый элемент
+                weatherList.add(
+                    Weather(
+                        item.getInt("dt"),
+                        item.getJSONObject("main").getDouble("temp"),
+                        item.getJSONObject("main").getInt("humidity"),
+                        weather.getString("icon"),
+                        weather.getString("description"),
+                        item.getJSONObject("wind").getDouble("speed"),
+                        item.getJSONObject("wind").getInt("deg"),
+                        item.getString("dt_txt")
+                    )
+                )
+            }
+
+            runOnUiThread {
+                // уведомляем визуальный элемент, что данные изменились
+                dailyInfoRecyclerView.adapter?.notifyDataSetChanged()
+            }
+        }
+        else
+            Log.d("KEILOG", error)
+    }
+    ```
+
+### Вывод списка (RecyclerView)
+
+**RecyclerView** - рекомендованный способ вывода списков (с его помощью можно было теоретически заполнить и список городов, но он для этого слишком монструозный). Его особенность в том, что визуальные элементы списка существуют только при отображении на экране. При выходе за экран визуальный элемент удаляется, перед появлением создается заново. Таким образом экономится память.
+
+Итак, на форме у нас уже лежит элемент **RecyclerView** (№2)
+
+В класс главного окна добавим переменную для связи с элементом **RecyclerView**
+
+```kt
+private lateinit var dailyInfoRecyclerView: RecyclerView
+```
+
+Затем в конструкторе её инициализируем и назначаем *layoutManager* и *adapter*
+
+```kt
+dailyInfoRecyclerView = findViewById(R.id.dailyInfoRecyclerView)
+
+// назначаем менеджер разметки
+dailyInfoRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
+
+// создаем адаптер
+val weatherAdapter = WeatherAdapter(weatherList, onIconLoad)
+
+// при клике на элемент списка показать подробную информацию (сделайте сами)
+weatherAdapter.setItemClickListener {
+    Log.d("KEILOG", "Click on ${it.weatherIcon}")
+}
+
+dailyInfoRecyclerView.adapter = weatherAdapter
+```
+
+Класс **WeatherAdapter** мы должны написать сами. Я положил его в шпаргалки.
+
+Разметка для элемента списка (не полная)
+
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    android:id="@+id/container"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="100dp"
+    android:layout_height="match_parent">
+
+    <ImageView
+        android:id="@+id/weather_icon"
+        android:layout_width="match_parent"
+        android:layout_height="100dp"/>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Температура"
+        />
+    <TextView
+        android:id="@+id/weather_temp"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>
+```
+

BIN
img/04032.png


+ 1 - 1
readme.md

@@ -30,7 +30,7 @@
 
 <div style="page-break-after: always;"></div>
 
-# http://gogs.kolei.ru/ekolesnikov/PiRIS
+# https://github.com/kolei/PiRIS
 
 # Содержание
 

+ 82 - 0
shpora/WeatherAdapter.kt

@@ -0,0 +1,82 @@
+package ru.yotc.myapplication
+
+import android.app.Activity
+import android.content.Context
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import java.lang.Exception
+import java.util.ArrayList
+
+/**
+ * Класс адаптера наследуется от RecyclerView.Adapter с указанием класса,
+ * который будет хранить ссылки на виджеты элемента списка, т.е. класса, реализующего ViewHolder.
+ * В нашем случае класс объявлен внутри класса адаптера.
+ *
+ * В параметры основного конструктора передаем список c данными о погоде и указатель на активити главного окна
+ * дело в том, что runOnUiThread работает только в контексте активити
+ */
+class WeatherAdapter(
+    private val values: ArrayList<Weather>,
+    private val activity: Activity
+): RecyclerView.Adapter<WeatherAdapter.ViewHolder>(){
+
+    // обработчик клика по элементу списка (лямбда выражение), может быть не задан
+    private var itemClickListener: ((Weather) -> Unit)? = null
+
+    fun setItemClickListener(itemClickListener: (Weather) -> Unit) {
+        this.itemClickListener = itemClickListener
+    }
+    
+    // Метод onCreateViewHolder вызывается при создании визуального элемента
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        // грузим layout, который содержит вёрстку элемента списка (нарисуйте сами)
+        val itemView = LayoutInflater
+            .from(parent.context)
+            .inflate(R.layout.weather_item,
+                parent,
+                false)
+
+        // создаем на его основе ViewHolder
+        return ViewHolder(itemView)
+    }
+
+    override fun getItemCount(): Int = values.size
+
+    // заполняет визуальный элемент данными
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        holder.tempTextView.text = "${values[position].mainTemp} C"
+        // onIconLoad.invoke(holder.iconImageView, values[position].weatherIcon)
+
+        holder.container.setOnClickListener {
+            //кликнули на элемент списка
+            itemClickListener?.invoke(values[position])
+        }
+
+        HTTP.getImage("https://openweathermap.org/img/w/${values[position].weatherIcon}.png") { bitmap, error ->
+            if (bitmap != null) {
+                activity.runOnUiThread {
+                    try {
+                        holder.iconImageView.setImageBitmap(bitmap)
+                    } catch (e: Exception) {
+
+                    }
+                }
+            } else
+                Log.d("KEILOG", error)
+        }
+    }
+
+    //Реализация класса ViewHolder, хранящего ссылки на виджеты.
+    class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
+        var iconImageView: ImageView = itemView.findViewById(R.id.weather_icon)
+        var tempTextView: TextView = itemView.findViewById(R.id.weather_temp)
+        var container: LinearLayout = itemView.findViewById(R.id.container)
+    }
+}
+