|
|
@@ -16,6 +16,30 @@
|
|
|
- [Сохранение состояния](#при-первом-запуске-приложения-первым-отображается-signup-screen-при-последующих---signin-01-балла)
|
|
|
- [Экран авторизации](#экран-авторизации)
|
|
|
- [Главный экран](#главный-экран)
|
|
|
+ - [Запрос подборки фильмов](#запрос-подборки-фильмов)
|
|
|
+ - [Скруглённые углы у постера](#скруглённые-углы-у-постера)
|
|
|
+ - [Переход между экранами](#переход-между-экранами)
|
|
|
+ - [Экран "профиль пользователя"](#экран-профиль-пользователя)
|
|
|
+ - [Запрос данных пользователя](#запрос-данных-пользователя)
|
|
|
+ - [Изменение аватарки пользователя](#изменение-аватарки-пользователя)
|
|
|
+ - [Выбор источника с помощью диалогового окна](#выбор-источника-камера-или-галерея-с-помощью-диалогового-окна)
|
|
|
+ - [Открытие Галереи](#открытие-галереи)
|
|
|
+ - [Отправка MultiPart запроса](#отправка-multipart-запроса)
|
|
|
+ - [Обрезка аватарки по размеру контейнера](#обрезка-аватарки-по-размеру-контейнера)
|
|
|
+ - [Кнопки "Обсуждения", "История", "Настройки" соответствуют макету](#кнопки-обсуждения-история-настройки-соответствуют-макету)
|
|
|
+ - [Экран списка чатов пользователя (Chat List Screen)](#экран-списка-чатов-пользователя-chat-list-screen)
|
|
|
+ - [Запрос списка чатов пользователя](#запрос-списка-чатов-пользователя)
|
|
|
+ - [Запрос сообщений чата](#запрос-сообщений-чата)
|
|
|
+ - [Отображение постера или аббревиатуры фильма](#отображение-постера-или-аббревиатуры-фильма)
|
|
|
+ - [Вывод двух строк в TextView](#вывод-двух-строк-в-textview)
|
|
|
+ - [Экран выбранного чата (Chat Screen)](#экран-выбранного-чата-chat-screen)
|
|
|
+ - [Упорядочивание (сортировка) списка](#упорядочивание-сортировка-списка)
|
|
|
+ - [Вывод даты перед группой сообщений](#вывод-даты-перед-группой-сообщений)
|
|
|
+ - [Группировка сообщений пользователя](#группировка-сообщений-пользователя)
|
|
|
+ - [Экран коллекций](#экран-коллекций)
|
|
|
+ - [Хранение информация о коллекциях в памяти устройства](#хранение-информация-о-коллекциях-в-памяти-устройства)
|
|
|
+ - [Список иконок плиткой](#список-иконок-плиткой)
|
|
|
+ - [Экран создания коллекций](#экран-создания-коллекций)
|
|
|
|
|
|
* **Модуль 2** - разработка приложения для часов (может быть и для телевизора). Коротенькое задание (30 минут). Нужно просто создать приложение из двух окон + работа с интернетом.
|
|
|
* **Модуль 3** - презентация.
|
|
|
@@ -453,11 +477,34 @@ Email проверяется на удовлетворение шаблону и
|
|
|
|
|
|
Для вывода списка фильмов используйте компонент **RecyclerView** (направление пролистывания не указано, так что делайте как вам больше нравится)
|
|
|
|
|
|
+### Скруглённые углы у постера
|
|
|
+
|
|
|
+Есть вариант с программным изменением картинки, но он не учитывает последующую возможную обрезку картинки (понадобится при выводе аватарки).
|
|
|
+
|
|
|
+Есть более простой и наглядный вариант: элемент **ImageView** заворачивается в контейнер **androidx.cardview.widget.CardView**, которому и задаётся радиус:
|
|
|
+
|
|
|
+```xml
|
|
|
+<androidx.cardview.widget.CardView
|
|
|
+ app:cardCornerRadius="100dp"
|
|
|
+>
|
|
|
+
|
|
|
+ <ImageView
|
|
|
+ android:layout_width="match_parent"
|
|
|
+ android:layout_height="match_parent"
|
|
|
+ />
|
|
|
+</androidx.cardview.widget.CardView>
|
|
|
+```
|
|
|
+
|
|
|
### Переход между экранами
|
|
|
|
|
|

|
|
|
|
|
|
-Для перехода между экранами внизу экарана есть панель навигации. Для её реализации есть отдельныё механизм, который мы пока не рассматривали. Вы можете сюда поместить горизонтальный **LinearLayout**.
|
|
|
+Для перехода между экранами внизу главного экрана есть панель навигации. Для её реализации есть отдельный механизм, который мы пока не рассматривали. Вы можете сюда поместить горизонтальный **LinearLayout**.
|
|
|
+
|
|
|
+## Экран "профиль пользователя"
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
>6. Реализуйте экран Profile Screen согласно макету:
|
|
|
> * Данные о пользователе необходимо запрашивать с сервера.
|
|
|
@@ -469,12 +516,251 @@ Email проверяется на удовлетворение шаблону и
|
|
|
> * При нажатии на кнопку "Выход" необходимо осуществлять переход на экран авторизации.
|
|
|
> * При нажатии на кнопку «Обсуждения» необходимо переходить на соответствующий экран.
|
|
|
|
|
|
+Критерий | Баллы
|
|
|
+---------|:----:
|
|
|
+Изображения отображаются без искажений | 0.1
|
|
|
+Радиус скругления углов кнопки "Выход" соответствует макету | 0.1
|
|
|
+Реализован запрос информации о пользователе. Запрос фиксируется сервером | 0.5
|
|
|
+Реализована отправка аватара. Запрос фиксируется сервером | 1
|
|
|
+При получении ошибки от сервера она отображается | 0.3
|
|
|
+Аватар пользователя обрезается | 0.2
|
|
|
+Аватар пользователя загружается с сервера | 0.2
|
|
|
+Кнопки "Обсуждения", "История", "Настройки" соответствуют макету оценивается верстка): для каждой кнопки иконка, заголовок, стрелка (минус 0,2 за каждый отсутствующий, неполный или некорректный элемент) | 0.6
|
|
|
+Реализован выбор источника фотографии | 0.2
|
|
|
+**Итого** | 3.2
|
|
|
+
|
|
|
+## Запрос данных пользователя
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+Если предыдущие запросы были доступны всем, то этот запрос может делать только авторизованный пользователь.
|
|
|
+
|
|
|
+Для того, чтобы запрос был авторизованным, нужно в заголовок запроса добавить параметр *Authorization*, как описано в начале документа.
|
|
|
+
|
|
|
+Напоминаю синтаксис добавления заголовка в классе **http**:
|
|
|
+
|
|
|
+```kt
|
|
|
+Http.call(
|
|
|
+ Http.buildRequest(
|
|
|
+ "http://cinema.kolei.ru/user",
|
|
|
+ headers = mapOf("Authorization" to "Bearer ${app.token}")
|
|
|
+ ),
|
|
|
+ userCallback
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+Успешный запрос вернёт **массив** объектов (с одним элементом, естественно), учитываёте при разборе JSON.
|
|
|
+
|
|
|
+Чтобы протестировать этот запрос в **swagger**-e, нужно получить токен авторизации запросом **/auth/login**, затем кликнуть кнопку **Authorize** на окне выше и ввести токен в поле **Value**:
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+Иконка замка сменится на "закрыто". После этого можно выполнять запросы, требующие авторизации.
|
|
|
+
|
|
|
+В ответ на запрос информации о пользователе придёт что-то подобное:
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+## Изменение аватарки пользователя
|
|
|
+
|
|
|
+### Выбор источника (камера или галерея) с помощью диалогового окна
|
|
|
+
|
|
|
+С диалоговыми окнами мы уже знакомы (мы их используем для вывода текста ошибок). У класса **AlertDialog** есть метод, позволяющий сделать выбор из массива:
|
|
|
+
|
|
|
+```kt
|
|
|
+// сначала объявляем массив строк для выбора
|
|
|
+val choiceItems = arrayOf("Галерея","Камера")
|
|
|
+
|
|
|
+// создаём и показываваем диалог
|
|
|
+AlertDialog.Builder(this)
|
|
|
+ .setTitle("Выберите источник")
|
|
|
+ .setNegativeButton("Отмена", null)
|
|
|
+ .setSingleChoiceItems(choiceItems, -1){
|
|
|
+ dialog, index ->
|
|
|
+ Toast.makeText(this, "$index", Toast.LENGTH_LONG).show()
|
|
|
+ dialog.dismiss()
|
|
|
+ }
|
|
|
+ .create()
|
|
|
+ .show()
|
|
|
+```
|
|
|
+
|
|
|
+При создании диалога добавился вызов метода *setSingleChoiceItems*, который как раз и задаёт массив элементов для выбора. **Первым** параметром этого метода задаётся массив **строк**, **вторым** - активный по-умолчанию элемент (можно указать `-1`, если не нужно выбирать что-то по-умолчанию) и **третьим** параметром задаётся лямбда функция, которая вызывается при выборе элемента списка.
|
|
|
+
|
|
|
+В лямбда функцию передаётся два параметра:
|
|
|
+* *dialog* - указатель на экземпляр диалога
|
|
|
+* *index* - позиция выбранного элемента в массиве
|
|
|
+
|
|
|
+Я в примере выше просто вывожу на экран номер выбранного элемента и закрываю диалог (*dialog.dismiss()*). Вам нужно в зависимости от выбранного элемента либо показать Галерею, либо открыть приложение Камера (можно прямо тут, но можно и выделить в отдельную лямбда функцию).
|
|
|
+
|
|
|
+### Открытие Галереи
|
|
|
+
|
|
|
+Открытие внешних программ (активностей) осуществляется тем же вызовом класса **Intent**. Только в параметрах мы должны передавать не контекст (*this*), а тип нужной нам активности.
|
|
|
+
|
|
|
+Для получения каких-то данных используется тип **Intent.ACTION_PICK**.
|
|
|
+
|
|
|
+Для конкретизации типа данных указывается свойство *type*
|
|
|
+
|
|
|
+Для открытия активности, которая может вернуть список картинок:
|
|
|
+
|
|
|
+```kt
|
|
|
+// константа для анализа результата объявляется на уровне класса
|
|
|
+val GALLERY_REQUEST = 1
|
|
|
+
|
|
|
+val photoPickerIntent = Intent(Intent.ACTION_PICK)
|
|
|
+// фильтр
|
|
|
+photoPickerIntent.type = "image/jpg"
|
|
|
+
|
|
|
+//запускаем запрос, указав что ждём результат
|
|
|
+startActivityForResult(photoPickerIntent, GALLERY_REQUEST)
|
|
|
+```
|
|
|
+
|
|
|
+Получение результата от активности мы уже разбирали, когда делали выбор города в проекте "погода". Обработчик результата всего один для класса активности. Поэтому при запуске активности мы и передаём уникальный номер запроса, чтобы при разборке ответа знать от кого он пришёл:
|
|
|
+
|
|
|
+```kt
|
|
|
+override fun onActivityResult(
|
|
|
+ requestCode: Int,
|
|
|
+ resultCode: Int,
|
|
|
+ intent: Intent?)
|
|
|
+{
|
|
|
+ // проверяем код запроса
|
|
|
+ if (requestCode == GALLERY_REQUEST) {
|
|
|
+ // убеждаемся что выполнено успешно (пользователь мог ничего и не выбрать в галерее)
|
|
|
+ if (resultCode == Activity.RESULT_OK && intent != null) {
|
|
|
+ // галерея возвращает URI картинки в свойстве data параметра intent
|
|
|
+ sendFile(intent.data!!)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Отправка MultiPart запроса
|
|
|
+
|
|
|
+>Класс **StreamHelper**, используемый при получении файла, лежит в [шпаргалках](../shpora/StreamHelper.kt)
|
|
|
+
|
|
|
+```kt
|
|
|
+private fun sendFile(fileUri: Uri) {
|
|
|
+ // получаем из Uri файла указатель на поток данных
|
|
|
+ val fileStream = contentResolver
|
|
|
+ .openInputStream(fileUri)
|
|
|
+
|
|
|
+ // получаем файл
|
|
|
+ val fileBody: RequestBody = StreamHelper
|
|
|
+ .create(
|
|
|
+ "image/jpg".toMediaType(),
|
|
|
+ fileStream!!)
|
|
|
+
|
|
|
+ val requestBody = MultipartBody.Builder()
|
|
|
+ .setType(MultipartBody.FORM)
|
|
|
+ .addFormDataPart("token", app.token)
|
|
|
+ .addFormDataPart(
|
|
|
+ "file", // название части запроса
|
|
|
+ "filename.jpg", // имя файла
|
|
|
+ fileBody // тело файла
|
|
|
+ )
|
|
|
+ .build()
|
|
|
+
|
|
|
+ val request = Request.Builder()
|
|
|
+ .url("http://cinema.kolei.ru/user/avatar")
|
|
|
+ .post(requestBody)
|
|
|
+ .build()
|
|
|
+
|
|
|
+ Http.call(request) { response, error ->
|
|
|
+ try {
|
|
|
+ if (error != null) throw error
|
|
|
+ if (!response!!.isSuccessful)
|
|
|
+ throw Exception(response.message)
|
|
|
+
|
|
|
+ // всё ОК
|
|
|
+ runOnUiThread {
|
|
|
+ /* при успешной отправке меняем аватарку
|
|
|
+ (ImageView в вашей активности)*/
|
|
|
+ avatarImageView.setImageURI( fileUri )
|
|
|
+ }
|
|
|
+ } catch (e: Exception) {
|
|
|
+ showAlert(e.message!!)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Обрезка аватарки по размеру контейнера
|
|
|
+
|
|
|
+При выборе файла из галереи пропорции будут скорее всего не одинаковые, поэтому при отображении вы должны задать как отображается изображение:
|
|
|
+
|
|
|
+```xml
|
|
|
+<ImageView
|
|
|
+ android:layout_width="88dp"
|
|
|
+ android:layout_height="88dp"
|
|
|
+ android:adjustViewBounds="true"
|
|
|
+ android:scaleType="centerCrop"
|
|
|
+```
|
|
|
+
|
|
|
+Параметр `android:adjustViewBounds="true"` включает масштабирование, а параметр `android:scaleType` указывает тип масштабирования. Вариантов масштабирования несколько, вы можете посмотреть на практике как они влияют на результат. Но нам нужен *centerCrop*, означающий, что картинка будет центрироваться, а то, что не помещается отрежется.
|
|
|
+
|
|
|
+### Кнопки "Обсуждения", "История", "Настройки" соответствуют макету
|
|
|
+
|
|
|
+Тут всё просто: в горизонтальный **LinearLayout** помещаете три элемента, как в вёрстке, задаёте этому **LinearLayout**-у тег `id` и событие *setOnClickListener* "вешаете" на **LinearLayout**. (Событие *setOnClickListener* объявлено в базовом классе **View**, от которого наследуются все визуальные элементы)
|
|
|
+
|
|
|
+## Экран списка чатов пользователя (Chat List Screen)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
>7. Реализуйте экран Chat List Screen согласно макету:
|
|
|
> * Информацию необходимо запрашивать с сервера (запрос чатов пользователя). Если информация с сервера содержит дубляжи – их необходимо удалить. Если ваш пользователь еще не имеет чатов, сервер присылает пустой список, отправьте сообщение от текущего пользователя в чате с id = 1 с помощью Swagger или Postman.
|
|
|
> * В подзаголовке ячейки необходимо отобразить последнее сообщение в соответствующем чате + имя его автора. Текст сообщения необходимо обрезать до двух строк.
|
|
|
> * Реализуйте отображение постеров к фильмам. Для получения постеров используйте подходящий запрос из API. Если постер для данного фильма нельзя получить из API - сгенерируйте абревиатуру по следующему правилу: если название фильма состоит и одного слова - необходимо взять первые две буквы слова; иначе - первые буквы первого и второго слова.
|
|
|
> * При нажатии на ячейку необходимо осуществлять переход на Chat Screen для выбранного фильма.
|
|
|
|
|
|
+Критерий | Баллы
|
|
|
+---------|:----:
|
|
|
+Реализован запрос списка чатов пользователя | 0.5
|
|
|
+Реализован запрос сообщений чата | 0.5
|
|
|
+Ячейка таблицы соответствует макету (оценивается верстка).Корректно реализованы 3 элемента: изображение, название, подзаголовок (минус 0,1 за каждый отсутствующий или
|
|
|
+некорректный элемент). | 0.3
|
|
|
+В подзаголовке отображается последнее сообщение
|
|
|
+для данного чата | 0.2
|
|
|
+Текст сообщения обрезается до двух строк | 0.2
|
|
|
+Реализовано отображение аббревиатуры согласно Заданию | 0.3
|
|
|
+**Итого** | 2
|
|
|
+
|
|
|
+### Запрос списка чатов пользователя
|
|
|
+
|
|
|
+Нужно учитывать, что запрос требует авторизации, а в остальном ничего особенного.
|
|
|
+
|
|
|
+>Учитывая, что в информации о чате мы должны показать последнее сообщение из чата, в дата класс надо добавить и свойство для этого сообщения
|
|
|
+
|
|
|
+### Запрос сообщений чата
|
|
|
+
|
|
|
+В информации о чате мы должны показать последнее сообщение этого чата, поэтому после получения списка чатов мы должны по каждому из них запросить и список сообщений этого чата. Последнее сообщение (тут желательно сделать проверку по дате) этого списка вписать в элемент списка чатов (ищем по Id чата)
|
|
|
+
|
|
|
+`GET {{base_url}}/chats/{{chatId}}/messages`
|
|
|
+
|
|
|
+### Отображение постера или аббревиатуры фильма
|
|
|
+
|
|
|
+Для вывода постера нужно иметь список фильмов. Можно запросить его ещё раз, а можно в главном окне хранение фильмов сделать не в свойствах класса активности, а в свойствах класса приложения (там, где вы храните токен)
|
|
|
+
|
|
|
+<!-- TODO сделать аббревиатуру названия фильма -->
|
|
|
+
|
|
|
+### Вывод двух строк в TextView
|
|
|
+
|
|
|
+Просто добавить атрибут *lines*:
|
|
|
+
|
|
|
+```xml
|
|
|
+<TextView
|
|
|
+ android:lines="2"
|
|
|
+ ...
|
|
|
+```
|
|
|
+
|
|
|
+### Выход из экрана
|
|
|
+
|
|
|
+По клику на стрелку влево (в верхней строке окна) вызвать метод *finish()* активности.
|
|
|
+
|
|
|
+## Экран выбранного чата (Chat Screen)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
>8. Реализуйте экран Chat Screen согласно макету:
|
|
|
> * Сообщения необходимо упорядочить от старых к новым сверху вниз. Для сегодняшних сообщений необходимо отобразить заголовок "Сегодня".
|
|
|
> * "Облако" сообщения должно растягиваться по содержимому.
|
|
|
@@ -483,14 +769,98 @@ Email проверяется на удовлетворение шаблону и
|
|
|
> * При нажатии на кнопку "Отправить" необходимо отправить сообщение на сервер. При позитивном ответе от сервера необходимо отобразить сообщение в чате. При возникновении ошибки - отобразить ошибку с помощью диалогового окна.
|
|
|
> * Необходимо валидировать поле для ввода на пустоту. При отсутствии текста сообщения необходимо отобразить ошибку с помощью диалогового окна.
|
|
|
|
|
|
+Критерий | Баллы
|
|
|
+---------|:----:
|
|
|
+Реализован запрос сообщений чата | 0.5
|
|
|
+Реализована отправка сообщения в чат | 0.7
|
|
|
+Отображение сообщения соответствует макету (оценивается верстка). Корректно реализованы 3 элемента: аватар, текст, подзаголовок (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.3
|
|
|
+Сообщения упорядочены согласно Конкурсному Заданию | 0.3
|
|
|
+"Облако" сообщения растягивается по содержимому без искажений радиуса скругления углов | 0.4
|
|
|
+Отображение последовательно идущих сообщений одного автора соответствует макету (расстояния между сообщениями меньше) | 0.4
|
|
|
+Поле для ввода растягивается при добавлении текста | 0.4
|
|
|
+При получении ошибки от сервера она отображается с помощью диалогового окна | 0.3
|
|
|
+При попытке отправить пустое сообщение отображается ошибка с помощью диалогового окна | 0.3
|
|
|
+**Итого** | 3.6
|
|
|
+
|
|
|
+Как получить список сообщений мы уже разбирали в прошлой лекции.
|
|
|
+
|
|
|
+### Упорядочивание (сортировка) списка
|
|
|
+
|
|
|
+Для того, чтобы массив объектов (экземпляров какого-либо класса) можно было сортировать, класс должен реализовывать интерфейс **Comparable** и переопределить метод *compareTo*:
|
|
|
+
|
|
|
+```kt
|
|
|
+data class ChatMessage(
|
|
|
+ val chatId: String,
|
|
|
+ val messageId: String,
|
|
|
+ val creationDateTime: LocalDateTime
|
|
|
+ // тут остальные поля
|
|
|
+ ): Comparable<ChatMessage>
|
|
|
+{
|
|
|
+ override fun compareTo(other: ChatMessage): Int {
|
|
|
+ return if(other.creationDateTime > this.creationDateTime) 1 else -1
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+...
|
|
|
+
|
|
|
+// после заполнения списка сообщений просто вызвать метод sort()
|
|
|
+chatMessageList.sort()
|
|
|
+```
|
|
|
+
|
|
|
+### Вывод даты перед группой сообщений
|
|
|
+
|
|
|
+1. В вёрстке элемента сообщения должен быть элемент для даты
|
|
|
+2. В адаптере **RecyclerView** сравнивать дату текущего сообщения с датой предыдущего (учитывайте, что предыдущего может не быть). Если дата отличается, то выводим дату, если не отличатется, то скрываем элемент даты (`visible = View.GONE`)
|
|
|
+
|
|
|
+### Группировка сообщений пользователя
|
|
|
+
|
|
|
+В принципе тоже самое что и с датой, только менять верхнюю границу сообщения
|
|
|
+
|
|
|
+```kt
|
|
|
+// считываем текущие параметры разметки элемента
|
|
|
+val param = messageTextView.layoutParams as ViewGroup.MarginLayoutParams
|
|
|
+
|
|
|
+// устанавливаем нужную границу
|
|
|
+param.setMargins(10,10,10,10)
|
|
|
+
|
|
|
+// применяем изменившиеся параметры к элементу
|
|
|
+messageTextView.layoutParams = param
|
|
|
+```
|
|
|
+
|
|
|
+## Экран коллекций
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
>9. Реализуйте экран Collections Screen согласно макету:
|
|
|
> * При нажатии на иконку в правом верхнем углу необходимо переходить на экран Create Collection Screen.
|
|
|
> * На экране необходимо отображать созданные коллекции. Информация о коллекциях должна храниться в памяти устройства. Необходимо хранить название коллекции и иконку.
|
|
|
> * Реализуйте Swipe-to-delete для удаления коллекции, в том числе из памяти устройства.
|
|
|
|
|
|
+Критерий | Баллы
|
|
|
+---------|:----:
|
|
|
+Ячейка коллекции соответствует макету (оценивается верстка). Корректно реализованы 3 элемента: иконка, название, стрелка (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.3
|
|
|
+Реализована возможность удаления коллекции с помощью swipe-to-delete | 0.3
|
|
|
+**Итог** | 0.6
|
|
|
+
|
|
|
+### Хранение информация о коллекциях в памяти устройства
|
|
|
+
|
|
|
+Вспоминаем метод *getSharedPreferences* - список коллекций можно хранить JSON-строкой (напоминаю, что хранилище оперирует скалярными типами). В качестве идентификатора иконки использовать Id ресурса.
|
|
|
+
|
|
|
+### Список иконок плиткой
|
|
|
+
|
|
|
+https://developer.android.com/codelabs/kotlin-android-training-grid-layout#0
|
|
|
+
|
|
|
+https://github.com/google-developer-training/android-kotlin-fundamentals-apps/tree/master/RecyclerViewGridLayout
|
|
|
+
|
|
|
+## Экран создания коллекций
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
>10. Реализуйте экран Create Collection Screen согласно макету:
|
|
|
> * При открытии экрана в качестве иконки должно быть выбрано случайное изображение из коллекции иконок.
|
|
|
> * При нажатии на кнопку "Выбрать иконку" необходимо осуществлять переход на экран Icon Selection. Реализуйте данный экран в соответствии с макетом.
|
|
|
> * При нажатии на кнопку "Сохранить" необходимо сохранить новую коллекцию в памяти устройства и закрыть экран.
|
|
|
> * Необходимо проверять на пустоту поле для ввода названия коллекции. При отсутствии значения необходимо отобразить сообщение об ошибке.
|
|
|
|
|
|
+Критерий | Баллы
|
|
|
+---------|:----:
|