Răsfoiți Sursa

продолжение погоды

Евгений Колесников 4 ani în urmă
părinte
comite
a33915aeaf
5 a modificat fișierele cu 264 adăugiri și 2 ștergeri
  1. 8 1
      articles/themes.md
  2. 17 1
      articles/weather.md
  3. 145 0
      articles/weather2.md
  4. 1 0
      readme.md
  5. 93 0
      shpora/Locator.kt

+ 8 - 1
articles/themes.md

@@ -3,7 +3,7 @@
 </a></td><td style="width: 20%;">
 <a href="../readme.md">Содержание
 </a></td><td style="width: 40%;">
-<a href="../articles/themes.md">Темы. Фигуры. Обработчики событий.
+<a href="../articles/weather.md">Проект погода (начало): геолокация, http(s)-запросы, разбор json, ImageView.
 </a></td><tr></table>
 
 `&lt;` `&gt;`
@@ -431,3 +431,10 @@ fun onOperatorClick(view: View) {
 * сделать кнопки со скругленными углами используя фигуры
 * создайте обработчики нажатия кнопок и реализуйте работу калькулятора
 
+<table style="width: 100%;"><tr><td style="width: 40%;">
+<a href="../articles/android_studio.md">Первый проект в Android Studio
+</a></td><td style="width: 20%;">
+<a href="../readme.md">Содержание
+</a></td><td style="width: 40%;">
+<a href="../articles/weather.md">Проект погода (начало): геолокация, http(s)-запросы, разбор json, ImageView.
+</a></td><tr></table>

+ 17 - 1
articles/weather.md

@@ -1,3 +1,11 @@
+<table style="width: 100%;"><tr><td style="width: 40%;">
+<a href="../articles/themes.md">Стили и темы. Ресурсы. Фигуры. Обработчики событий.
+</a></td><td style="width: 20%;">
+<a href="../readme.md">Содержание
+</a></td><td style="width: 40%;">
+<a href="../articles/weather2.md">Проект погода (продолжение): SplashScreen (заставка). Выбор города. Выбор и отображение массива значений (почасовая, ежедневная)
+</a></td><tr></table>
+
 # Проект погода (начало): геолокация, http(s)-запросы, разбор json, ImageView.
 
 На примере "Калькулятора" мы познакомились с интерфейсом *Android Studio*, более-менее разобрались со структурой проекта андроид.
@@ -384,4 +392,12 @@ HTTP.getImage("https://openweathermap.org/img/w/${icoName}.png") { bitmap, error
 
 # Задание
 
-Разобрать все перечисленные выше параметры погоды и вывести их на экран
+Разобрать все перечисленные выше параметры погоды и вывести их на экран
+
+<table style="width: 100%;"><tr><td style="width: 40%;">
+<a href="../articles/themes.md">Стили и темы. Ресурсы. Фигуры. Обработчики событий.
+</a></td><td style="width: 20%;">
+<a href="../readme.md">Содержание
+</a></td><td style="width: 40%;">
+<a href="../articles/weather2.md">Проект погода (продолжение): SplashScreen (заставка). Выбор города. Выбор и отображение массива значений (почасовая, ежедневная)
+</a></td><tr></table>

+ 145 - 0
articles/weather2.md

@@ -0,0 +1,145 @@
+<table style="width: 100%;"><tr><td style="width: 40%;">
+<a href="../articles/themes.md">Стили и темы. Ресурсы. Фигуры. Обработчики событий.
+</a></td><td style="width: 20%;">
+<a href="../readme.md">Содержание
+</a></td><td style="width: 40%;">
+<a href="../articles/weather2.md">Проект погода (продолжение): SplashScreen (заставка). Выбор города. Выбор и отображение массива значений (почасовая, ежедневная)
+</a></td><tr></table>
+
+# Проект погода (продолжение): SplashScreen (заставка). Выбор города. Выбор и отображение массива значений (почасовая, ежедневная)
+
+## SplashScreen (заставка).
+
+Если приложение долго грузится (запрос геолокации или "тяжёлых" данных из сети), то принято при запуске показывать заставку.
+
+Есть два варианта:
+
+1. Рисуют дополнительную **activity** с заставкой и запускают её первой. 
+2. Прямо на основной **activity** рисуют **ImageView** поверх всех элементов и скрывают её, когда необходимость в ней исчезает.
+
+Рассмотрим второй вариант (при условии что у нас **ConstraintLayout**):
+
+Я в качестве заставки буду показывать фон:
+
+* первым элементом кладем **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" />
+```
+
+* в коде главного окна в момент, когда нужно убрать заставку меняем Z-индекс заставки
+
+    ```kt
+    val splash = findViewById<ImageView>(R.id.splash)
+    splash.elevation = 0F
+    ```
+
+    >чтобы картинка нам не мешалась при разработке, мы можем в дизайнере оставить Z-индекс **0**, а в конструкторе окна задать **999F**
+
+Тут надо помнить, что в вёрстке указываются **dp**, а в коде **float**
+
+## Выбор города
+
+>В принципе список можно вывести прямо в главном окне (на форму кинуть **ListView** и с помощью атрибута *elevation* поместить его поверх всех элементов), но в таком случае не будет реализован выход по кнопке *Назад*, что может сказаться на юзабилити. 
+
+Поэтому, будем реализовывать "классический" вариант со списком в отдельном **activity**. Заодно научимся запускать дополнительные **activity** и получать от них ответ.
+
+1. Создаем новую форму (**Activity**) с именем *CityListActivity*
+
+    ![Создаем новую форму](/img/as032.png)
+
+2. На главное окно добавляем кнопу перехода на экран выбора города и обработчик для нее:
+
+    ```kt
+    startActivityForResult( 
+        Intent(this, CityListActivity::class.java), 
+        1)
+    ```
+
+    Здесь *startActivityForResult* - метод запуска нужной **activity**. 
+
+3. На форму *CityListActivity* кидаем вертикальный **LinearLayout**, в него **TextView** (для заголовка "Выберите город") и **ListView**. **ListView** присваиваем id *cityList*
+
+    ![](/img/as035.png)
+
+
+4. В классе *CityListActivity* 
+
+    * создаем массив городов
+
+        ```kt
+        private var cityNames = arrayOf(
+            "Moscow",
+            "Yoshkar-Ola",
+            "Kazan"
+        )
+        ```
+
+    * в конструкторе получаем указатель на **ListView**
+
+        ```kt
+        cityListView = findViewById(R.id.cityList)
+        ```
+
+    * задаем для списка **ArrayAdapter**. **ArrayAdapter** связывает массив данных с шаблоном элемента списка.
+
+        ```kt
+        cityListView.adapter = ArrayAdapter(
+            this,
+            R.layout.city_list_item,
+            cityNames
+        )
+        ```
+
+        Android Studio покажет ошибку, что не знает что такое ``city_list_item`` - в контекстном меню добавляем реализацию:
+
+        ![Создание Layout для элемента списка](/img/as036.png)
+
+        **Внимание!** RootElement поменять на TextView
+
+        ![Создание Layout для элемента списка](/img/as037.png)
+
+        Созданный шаблон можно настроить (установить высоту, добавить границы...)
+
+    * задаем обработчик клика по элементу списка
+
+        cityListView.setOnItemClickListener { parent, view, position, id ->
+            // получаем название города
+            val cityName = cityNames[position]
+
+            // запоминаем выбранное название города в параметрах
+            val newIntent = Intent()
+            newIntent.putExtra("cityName", cityName)
+            setResult(RESULT_OK, newIntent)
+
+            // заверщаем текущий activity
+            finish();
+        }
+        ```
+
+5. В классе главного окна для получения результата выбора реализуем метод *onActivityResult*
+
+    ```kt
+    @SuppressLint("MissingSuperCall")
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (data == null) {
+            return
+        }
+        val name = data.getStringExtra("cityName")
+
+        // тут запускаем http-запрос по имени города
+    }
+    ```
+
+    >Метод *onActivityResult* гугл объявил устаревшим (deprecated), и в IDE он помечается как ошибка - надо в контекстном меню "More action..." выбрать "Supress: add...". Перед методом будет добавлена аннотация `@SuppressLint("MissingSuperCall")`.

+ 1 - 0
readme.md

@@ -138,6 +138,7 @@ http://sergeyteplyakov.blogspot.com/2014/01/microsoft-fakes-state-verification.h
 
 4. [Проект погода (начало): геолокация, http(s)-запросы, разбор json, ImageView.](./articles/weather.md)
 
+5. [Проект погода (продолжение): SplashScreen (заставка). Выбор города. Выбор и отображение массива значений (почасовая, ежедневная)](./articles/weather2.md)
 <!--
 
 https://office-menu.ru/uroki-sql Уроки SQL

+ 93 - 0
shpora/Locator.kt

@@ -0,0 +1,93 @@
+package ru.yotc.myapplication
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.Looper
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.google.android.gms.location.*
+
+/*
+
+В манифест добавить разрешеня на геолокацию
+
+<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+*/
+class MainActivity : AppCompatActivity() {
+    private lateinit var fusedLocationClient: FusedLocationProviderClient
+    private lateinit var mLocationRequest: LocationRequest
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+
+        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
+        checkPermission()
+    }
+
+    private fun checkPermission(){
+        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
+            ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
+        {
+            // нет разрешений
+            val permissions = arrayOf(
+                Manifest.permission.ACCESS_FINE_LOCATION,
+                Manifest.permission.ACCESS_COARSE_LOCATION
+            )
+            ActivityCompat.requestPermissions(this, permissions, 0)
+        } else {
+            mLocationRequest = LocationRequest()
+            mLocationRequest.interval = 10000
+            mLocationRequest.fastestInterval = 1000
+            mLocationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
+
+            fusedLocationClient.requestLocationUpdates(mLocationRequest, mLocationCallback, Looper.myLooper())
+        }
+    }
+
+    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
+                                            grantResults: IntArray) {
+        when (requestCode) {
+            1 -> {
+                if (!grantResults.isEmpty() && grantResults[0] ==
+                    PackageManager.PERMISSION_GRANTED) 
+                {
+                    if ((ContextCompat.checkSelfPermission(this@MainActivity, Manifest.permission.ACCESS_FINE_LOCATION) === PackageManager.PERMISSION_GRANTED))
+                    {
+                        // получили разрешение - снова запускаем запрос разрешений, чтобы попасть в цикл опроса геолокации
+                        checkPermission()
+                    }
+                } else {
+                    Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show()
+                }
+                return
+            }
+        }
+    }
+
+    private var mLocationCallback: LocationCallback = object : LocationCallback() {
+        override fun onLocationResult(locationResult: LocationResult) {
+            if (!locationResult.locations.isEmpty()) {
+                val locIndex = locationResult.locations.size - 1
+                val lon = locationResult.locations[locIndex].longitude
+                val lat = locationResult.locations[locIndex].latitude
+
+                onGetCoordinates(lat, lon)
+            }
+        }
+    }
+
+    fun onGetCoordinates(lat: Double, lon: Double){
+        // завершаем цикл опроса
+        fusedLocationClient.removeLocationUpdates(mLocationCallback)
+
+        // тут делаем что хотели с координатами
+        Toast.makeText(this, "${lat}, ${lon}", Toast.LENGTH_LONG).show()
+    }
+}