Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Наташ, ты спишь? - ограничиваем позиции компонентов

Немного вспомним историю. Где-то до версии Android 2.2 многие разработчики использовали картинку в качестве фона для экрана активности. Тогда моделей было не слишком много и они не сильно различались по размерам. Но потом популярность Android стала расти и появился зоопарк устройств с самыми разными характеристиками. Постепенно, желание использовать фоновую картинку исчезло. Для примера попробуем разместить первую попавшую картинку на устройстве Pixel 3 XL. Android Studio позволяет выбрать нужное устройство из списка, чтобы увидеть готовый результат.

Выбираем корневой элемент ConstraintLayout и атрибуту android:background присваиваем нужное изображение. Выбираем сверху из выпадающего списка различные устройства и смотрим, как это будет смотреться. Согласитесь, результат не очень. Картинка сжалась и стала некрасивой.

Наташ, ты спишь?

Забудьте об этом способе, им не пользуются. Однако, мы попробуем решить эту задачу немного другим способом. Заодно поближе познакомимся с разметкой ConstraintLayout.

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

По умолчанию, в студии уже есть папка res/drawable. Вы можете поместить туда картинку и использовать её, но это не совсем правильно, хотя и будет работать. Данная папка предназначена для других целей - в ней хранят векторные картинки в формате XML. А обычные картинки хранят в родственных папках с квалификаторами. Переключитесь в режим Project, найдите в иерархическом списке папку res, далее из контекстно меню выбираем New | Android Resource Directory.

Resource Directory

В диалоговом окне в секции Resource type выбираем Drawable, а в Available qualifiers - Density. Затем нажимаем кнопку с двумя стрелками право >>. На правой стороне появятся разные возможные варианты. Из выпадающего списка выбираем нужный вариант. Для устройств с самым высоким разрешением нужно создавать папку XXX-High Density. В самой верхней строке Directory Name вы увидите название будущей папки, например, drawable-xxxhdpi. Нажимаем OK и в списке папок res видим новую папку. Повторяем шаги для каждого разрешения, для которых вы подготовили картинки. Можно сразу создавать папку с нужным квалификатором через меню New | Directory, если вы точно помните имя папки. На данный момент устройств с низким и средним разрешением почти нет, поэтому обычно для них не делают картинки, решение остаётся на ваше усмотрение.

Resource

Копируем все картинки в подготовленные папки. Пора заняться своим проектом.

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

Resource

Чтобы привязать картинку к левому краю, нужно подвести курсор мыши к левому кружочку (он подсветится) и потащить к краю. ImageView прилипнет к левому краю. Аналогично поступаем с верхней частью.

В правой части можете проверить, что привязка произошла корректно.

Resource

Добавляем ещё один ImageView и привязываем его к нижнему левому углу, повторяя предыдущие действия.

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

Resource

Тоже самое для верхней правой картинки с отступом от верха. В целом должно получиться приблизительно так.

Resource

Теперь займёмся текстовыми блоками. Начнём с верхнего. С ним особых проблем нет - размещаем по центру по горизонтали и немного отступаем от верхнего края. Берём компонент TextView и пробуем. Помещаем текстовый блок приблизительно в центр макета. Далее из контекстного меню выбираем Center | Horizontally. В окне настроек подобная привязка отобразится как Horizontal Bias (0.5). Как делать отступ от верхнего края вы уже знаете. Выберите свой вариант.

Resource

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

Resource

Для размещения третьего текстового блока воспользуемся ограничителем Guideline. Это удобно, когда нужно привязаться к какому-то значению, не связанному с размещёнными компонентами. Тогда этот невидимый элемент поможет установить ограничительную черту и плясать от неё. Обычно, его используют, когда несколько элементов нужно выравнять по одной линии. Добавим вертикальный ограничитель. Наверху у него есть кружочек, который позволяет выбрать вариант отступа в пикселях или процентах. Допустим, мы выбрали 10% от ширины экрана и текстовый блок привяжем к нему.

Guideline

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

Четвёртую текстовую метку можно разместить в правом нижнем углу с небольшими отступами. Затем нижний край третьей метки привязываем к верхнему краю четвёртой метки. На этом размещение компонентов на макете не закончилось.

Займёмся пока внешним видом текстовых меток. Установим для них чёрный фон и белый шрифт, выберем подходящий размер шрифта, сделаем выравнивание текста по центру. Во время этих операции вы можете заметить, как плывёт наша вёрстка. Если там находится длинный текст, то текстовая метка начинает уходит за края экрана или наползать на другие элементы ненужным образом. Здесь вам придётся повозиться, устанавливая другие режимы для атрибутов layout_width и layout_height. На выбор есть фиксированный размер, гибкая подгонка, под содержимое. Подробнее есть в отдельной статье о ConstraintLayout.

У меня получился такой вариат (вы можете придумать свой).

ConstraintLayout

В текстовом виде это выглядит следующим образом.


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#B0ABA7"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/left_up_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="A cat in the left-up corner"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/cat_left_top" />

    <ImageView
        android:id="@+id/left_bottom_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="A cat in the left-bottom corner"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:srcCompat="@drawable/cat_left_bottom" />

    <ImageView
        android:id="@+id/right_bottom_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="24dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/cat_right_bottom" />

    <ImageView
        android:id="@+id/right_up_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/cat_right_top" />

    <TextView
        android:id="@+id/first_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="8dp"
        android:background="#000"
        android:text="Наташ, ты спишь?"
        android:textAlignment="center"
        android:textColor="#FFF"
        android:textSize="24sp"
        app:layout_constraintEnd_toStartOf="@+id/right_up_image"
        app:layout_constraintStart_toStartOf="@+id/guideline2"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/second_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:background="#000"
        android:singleLine="false"
        android:text="Уже 6 часов утра, Наташ"
        android:textAlignment="center"
        android:textColor="#FFF"
        android:textSize="24sp"
        app:layout_constraintEnd_toStartOf="@+id/right_up_image"
        app:layout_constraintStart_toEndOf="@+id/left_up_image"
        app:layout_constraintTop_toBottomOf="@+id/left_up_image" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.10218978" />

    <TextView
        android:id="@+id/third_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:background="#000"
        android:lines="2"
        android:text="Вставай, мы там всё уронили"
        android:textAlignment="center"
        android:textColor="#FFF"
        android:textSize="24sp"
        app:layout_constraintBottom_toTopOf="@+id/forth_text"
        app:layout_constraintEnd_toEndOf="@+id/left_bottom_image"
        app:layout_constraintStart_toStartOf="@+id/guideline" />

    <TextView
        android:id="@+id/forth_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="24dp"
        android:background="#000"
        android:singleLine="false"
        android:text="Мы уронили вообще всё, Наташ, честно"
        android:textAlignment="center"
        android:textColor="#FFF"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline2" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.33" />
</androidx.constraintlayout.widget.ConstraintLayout>

Смотрим результат в эмуляторе. На мой взгляд, довольно близко к оригинальному мему.

Emulator

Переходим к программной части. Добавим к изображению четвёртого кота возможность реагировать на нажатия и будем тыкать в наглую рыжую морду для смены текста в текстовых метках. По сути, ImageView превращается в кнопку. Я решил оставить верхнюю текстовую метку в покое, текст "Наташ, ты спишь?" останется нетронутой. Для остальных создадим список возможных вариантов. Я взял текст из двух популярных мемов, вы можете дополнить список другими выражениями. Далее при нажатии на рыжую морду список перемешивается и первые три элемента списка попадают в текстовые метки.


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
// http://developer.alexanderklimov.ru/android/

package ru.alexanderklimov.natasha

import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val secondText: TextView = findViewById(R.id.second_text)
        val thirdText: TextView = findViewById(R.id.third_text)
        val forthText: TextView = findViewById(R.id.forth_text)
        val rightBottomImage: ImageView = findViewById(R.id.right_bottom_image)

        // Обработчик щелчков
        rightBottomImage.setOnClickListener {
            val phrases = listOf(
                "Уже 6 часов утра, Наташ",
                "Вставай, мы там всё уронили",
                "Мы уронили вообще всё, Наташ, честно",
                "Наташ, ты чё опять лежишь?",
                "Часики-то тикают",
                "Мужика-то хоть нашла себе?",
                "Уже дитачек пора рожать вообще-то")

            val shuffledList = phrases.shuffled() // перемешанный список

            // Выводим в текстовые метки
            secondText.text = shuffledList[0]
            thirdText.text = shuffledList[1]
            forthText.text = shuffledList[2]
        }
    }
}

Теперь при каждом щелчке текст будет меняться. Наташа, просыпайся! Коты жрать хотят!

Готовое приложение

Вариант для Java


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.natasha;

import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.util.Random;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        TextView secondTextView = findViewById(R.id.second_text);
        TextView thirdTextView = findViewById(R.id.third_text);
        TextView fourTextView = findViewById(R.id.forth_text);

        ImageView right_bottom_image = findViewById(R.id.right_bottom_image);
        right_bottom_image.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String[] phrases = {"Уже 6 часов утра, Наташ",
                        "Вставай, мы там всё уронили",
                        "Мы уронили вообще всё, Наташ, честно",
                        "Наташ, ты чё опять лежишь?",
                        "Часики-то тикают",
                        "Мужика-то хоть нашла себе?",
                        "Уже дитачек пора рожать вообще-то"};
						
                shuffleArray(phrases); // перемешиваем

                secondTextView.setText(phrases[0]);
                thirdTextView.setText(phrases[1]);
                fourTextView.setText(phrases[2]);
            }
        });
    }

    void shuffleArray(String[] ar){
        Random rnd = new Random();
        for (int i = ar.length - 1; i > 0; i--) {
            int index = rnd.nextInt(i + 1);
            String a = ar[index];
            ar[index] = ar[i];
            ar[i] = a;
        }
    }
}

Дополнительное чтение

Реклама