Туториал по Drag and Drop в Android: перемещение вьюшек и данных

09 декабря 2021

Узнайте, как использовать фреймворк drag-and-drop в Android, чтобы обеспечить более полный пользовательский опыт на основе жестов.

Вы готовы порадовать своих пользователей фреймворком drag-and-drop для Android?

Эта функция кажется волшебной, потому что она дает пользователям вашего приложения возможность перемещать элементы пользовательского интерфейса, касаясь экрана. В этом туториале вы узнаете о событиях и действиях, управляющих этим фреймворком, чтобы вы стали мастером drag-and-drop! Вы создадите приложение под названием Masky, которое позволит вам переместить маску на экран и поместить ее на лицо без маски. В частности, вы узнаете, как:

1. Создавать операции drag-and-drop.
2. Настраивать тень.
3. Отвечать на события drag-and-drop.
4. Перемещать вью - маску - по экрану в новую область.
5. Проверять, находится ли маска на лице.

Приступим

Загрузите стартовый проект.
Откройте стартовый проект (starter project) в Android Studio. Выполните сборку и запустите. Вы увидите следующий экран:

В настоящее время приложение показывает маску и лицо без маски, но вы пока ничего не можете с ними сделать. Стартовый проект - это чистый лист, готовый для начала работы. По мере того, как вы будете следовать туториалу, вы увидите, как приложение обретает форму.

 

Процесс Drag-and-Drop

Платформа Android с функцией drag-and-drop позволяет пользователям перемещать данные и view с помощью графических жестов. Пользователи могут перемещать данные между view в одном приложении или даже из одного приложения в другое, если у них включен многооконный режим.

В этом туториале вы будете использовать внутренние компоненты фреймворка, такие как классы событий drag-and-drop и слушателей drag-and-drop, для разработки собственных действий drag-and-drop.

Четыре состояния Drag-and-Drop

Процесс drag-and-drop состоит из четырех состояний:

1. Started
2. Continuing
3. Dropped
4. Ended

Теперь взгляните на каждое из этих состояний, как показано на схеме ниже:

Состояние Started

Когда пользователь производит жест пользовательского интерфейса, который ваше приложение распознает как триггер, например длительное нажатие, начинается процесс перемещения. Приложение предоставляет данные перемещения вместе с обратным вызовом перемещения в качестве аргументов системе через метод startDragAndDrop().

Заметка

Чтобы начать перемещение, используйте startDragAndDrop() для устройств Nougat и новее. Для устройств до версии Nougat используйте startDrag().

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

Слушатель событий перемещения возвращает логическое значение true, чтобы продолжить прием событий перемещения. Если слушателю нужно знать только то, когда перемещение закончилось, он может отказаться от получения данных перемещения, вместо этого вернув false. Это гарантирует, что слушатель получит только последнее событие перемещения с типом действия ACTION_DRAG_ENDED.

Заметка

Следующие методы DragEvent недействительны для типа действия события ACTION_DRAG_STARTED:
getClipData()
getX()
getY()
getResult()

 

Состояние Continuing

Когда пользователь продолжает перемещение, процесс перемещения переходит в состояние continuing (продолжения).

В этом состоянии система отправляет одно или несколько событий перемещения зарегистрированным слушателям событий перемещения.
Например, когда drag shadow входит в ограничивающую рамку view, зарегистрированного для событий перемещения, система отправляет слушателю тип действия ACTION_DRAG_ENTERED.

После получения события ACTION_DRAG_ENTERED и до того, как он сможет получить событие ACTION_DRAG_EXITED, слушатель получает новое событие ACTION_DRAG_LOCATION по мере продолжения перемещения. Здесь вы можете получить текущие координаты x, y перемещаемого view.

Аналогичным образом, когда drag shadow покидает ограничивающую рамку, слушателю отправляется тип действия ACTION_DRAG_EXITED.
В вашем приложении вы будете иметь дело только с типами действий ACTION_DRAG_ENTERED и ACTION_DRAG_EXITED.

 

Состояние Dropped

Когда пользователь отпускает drag shadow над view, которое зарегистрировано для событий перемещения, система отправляет событие перемещения с типом действия ACTION_DROP.

Вы получаете данные, переданные из этого события перемещения, в качестве аргументов в методы startDragAndDrop()/startDrag().

 

Состояние Ended

Наконец, система завершает операцию перемещения, отправляя событие перемещения с типом действия ACTION_DRAG_ENDED.

После получения ACTION_DRAG_ENDED каждый слушатель событий перемещения должен:

1. Сбросить все изменения состояния или пользовательского интерфейса, произведенные во время операции перемещения.
2. Вернуть логическое значение true.
3. Дополнительно проверить статус успешного сброса, вызвав getResult().

Теперь, когда у вас есть теоретическое представление об операциях перемещения в Android, пора применить эти знания в реальном приложении!

 

Разработка операции перемещения

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

В этом приложении вы будете использовать длительное нажатие, чтобы начать операцию перемещения.

 

Добавление drag shadow (перемещение тени)

Система отображает изображение-заполнитель (изображение-плейсхолдер) для представления фактического вью/данных во время операции перемещения. Это представление изображения-заполнителя является перемещением тени (drag shadow).

Чтобы создать строителя drag shadow, вы создаете субкласс View.DragShadowBuilder. Затем вы можете передать builder в качестве аргумента системе при запуске операции перемещения с помощью startDragAndDrop() или startDrag().

Затем система использует drag shadow builder для вызова своих методов обратного вызова для получения drag shadow.

Если вы не хотите, чтобы drag shadow отображалась, то вы можете управлять отображением тени (drop shadow), выбирая соответствующий конструктор View.DragShadowBuilder:

1. View.DragShadowBuilder(view): принимает объект View для создания drag shadow, который похож на объект view, который перемещает пользователь. Чтобы настроить drag shadow, вы можете создать субкласс этого класса и переопределить методы, как вы увидите позже.
2. View.DragShadowBuilder(): этот drag shadow builder не имеет параметров и даст вам невидимую drag shadow. Когда пользователь перемещает вью, визуальных сигналов о том, что перемещение выполняется, нет, пока не закончится действие.

Следующим шагом является настройка drag shadow в соответствии с вашими требованиями путем создания субкласса View.DragShadowBuilder(view).

 

Настройка Drag Shadow

При использовании конструктора по умолчанию View.DragShadowBuilder(view) вы получите drag shadow по умолчанию. Значения по умолчанию в drag shadow имеют те же размеры, что и аргумент View.

Точка касания (touch point) - это место пальца пользователя в drag shadow. Точка касания по умолчанию находится в центре аргумента View.

Прежде чем вы сможете приступить к настройке drag shadow, вам необходимо импортировать следующие пакеты:

import android.graphics.Canvas
import android.graphics.Point
import android.view.View
import androidx.core.content.res.ResourcesCompat

Затем добавьте MaskDragShadowBuilder, вставив следующий код в MainActivity.kt:

private class MaskDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {

  //1
  private val shadow = ResourcesCompat.getDrawable(view.context.resources, R.drawable.ic_mask, view.context.theme)

  // 2
  override fun onProvideShadowMetrics(size: Point, touch: Point) {
    // 3
    val width: Int = view.width

    // 4
    val height: Int = view.height

    // 5
    shadow?.setBounds(0, 0, width, height)

    // 6
    size.set(width, height)

    // 7
    touch.set(width / 2, height / 2)
  }

  // 8
  override fun onDrawShadow(canvas: Canvas) {
    // 9
    shadow?.draw(canvas)
  }
}

Этот код создает drag shadow, которая выглядит как маска, которую перемещает пользователь. Вот как это работает:

1. Устанавливается внешний вид drag shadow в соответствии с фактической маской. Drag shadow здесь имеет тип Drawable.
2. Вызывается метод onProvideShadowMetrics() при запуске методов startDragAndDrop() или startDrag().
3. Определяется ширина drag shadow как полная ширина маски View.
4. Определяется высота drag shadow как полная высота маски View.
5. Устанавливаются размеры и положение смещения drag shadow на холсте (canvas).
6. Регулируются значения ширины и высоты параметра размера.
7. Устанавливается положение точки касания drag shadow в середину drag shadow
8. Вызывается метод onDrawShadow() после вызова метода onProvideShadowMetrics(). Метод onDrawShadow() рисует фактическую drag shadow на объекте холста, используя метрики из onProvideShadowMetrics().
9. Рисует drag shadow маски Drawable на холсте.

Теперь, когда у вас есть drag shadow, пора заняться слушателями событий перемещения.

 

Реализация слушателя событий перемещения (Drag Event Listener)

Когда выполняется процесс перемещения, система отправляет событие DragEvent всем зарегистрированным слушателям событий перемещения.

Затем вы реализуете View.OnDragListener для создания объекта слушателя событий перемещения, а затем установите слушатель на setOnDragListener()View.

 

Отправка событий перемещения

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

1. ACTION_DRAG_STARTED
2. ACTION_DRAG_ENTERED
3. ACTION_DRAG_LOCATION
4. ACTION_DRAG_EXITED
5. ACTION_DROP
6. ACTION_DRAG_ENDED

Вы можете прочитать сводку по каждому типу действий в официальной документации Android.

Чтобы получить доступ к константам типа действия, вызовите метод getAction() в DragEvent.

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

 

Старт операции перемещения

Перед тем, как начать операцию перемещения, ознакомьтесь с activity_main.xml. Этот файл содержит основной макет для функции перемещения, которую вы реализуете.

Сосредоточьтесь на следующих view:

  • Mask view: ImageView.
  • Unmasked face: талисман Bugdroid ImageView.
  • Mask drop area: ConstraintLayout, который представляет собой полную область внутри границы пунктирной линии, которая представляет собой ограничивающую рамку для событий входа/выхода перемещения.

Откройте MainActivity.kt и добавьте к нему метод attachViewDragListener(). Метод attachViewDragListener() определяет набор действий, которые необходимо выполнить, чтобы начать операцию перемещения.

Затем импортируйте следующие пакеты:

import android.content.ClipData
import android.content.ClipDescription
import android.os.Build
import android.os.Bundle

Затем вызовите метод attachViewDragListener() в обратном вызове жизненного цикла onCreate() вашей активности:

private fun attachViewDragListener() {

  // 1
  binding.mask.setOnLongClickListener { view: View ->

  // 2
  val item = ClipData.Item(maskDragMessage)

  // 3
  val dataToDrag = ClipData(
      maskDragMessage,
      arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
      item
  )

  // 4
  val maskShadow = MaskDragShadowBuilder(view)

  // 5
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
     //support pre-Nougat versions
     @Suppress("DEPRECATION")
     view.startDrag(dataToDrag, maskShadow, view, 0)
  } else {
     //supports Nougat and beyond
     view.startDragAndDrop(dataToDrag, maskShadow, view, 0)
  }

  // 6
  view.visibility = View.INVISIBLE

  //7
  true
 }
}

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

2. Создаёте ClipData и ClipData.Item, которые представляют данные, которые пользователь перемещает.

3. В ClipData вы включаете массив типов MIME, которые представляют данные, с помощью ClipDescription. Если вы не хотите поддерживать перемещение данных, вы можете просто передать нулевое значение для типов MIME.

4. Создаёте экземпляр drag shadow builder. Вам не нужно настраивать drag shadow, поэтому вы используете по умолчанию View.DragShadowBuilder(view).

5. Здесь вы предоставляете ClipData, drag shadow builder и view маски, которые хотите переместить в качестве аргументов для startDrag().

6. Скрываете view маски, когда начнется перемещение. Во время перемещения должна быть видна только drag shadow.

7. Возвращаете логическое значение true, чтобы сообщить системе, что событие касания было успешным.

Выполните сборку и запустите. Теперь вы, наконец, можете переместить маску.

 

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

 

Ответ на события перемещения

Чтобы реагировать на события перемещения, вам необходимо зарегистрировать слушателя событий перемещения в представлении maskDropArea. Только view, у которых установлены слушатели событий перемещения, могут реагировать на события перемещения.

Для начала откройте MainActivity.kt и добавьте следующее:

// 1
private val maskDragListener = View.OnDragListener { view, dragEvent ->

  //2
  val draggableItem = dragEvent.localState as View

  //3
  when (dragEvent.action) {
    DragEvent.ACTION_DRAG_STARTED -> {
      true
    }
    DragEvent.ACTION_DRAG_ENTERED -> {
      view.invalidate()
      true
    }
    DragEvent.ACTION_DRAG_LOCATION -> {
      true
    }
    DragEvent.ACTION_DRAG_EXITED -> {
      true
    }
    DragEvent.ACTION_DROP -> {
      true
    }
    DragEvent.ACTION_DRAG_ENDED -> {
      true
    }
    else -> {
      false
    }
  }
}

Приведенный выше код создает слушателя событий перемещения. Вот как это работает:

1. Создается экземпляр View.OnDragListener и присваивается переменной maskDragListener.
2. Получает ссылку на view маски.
3. Слушатель событий перемещения может получить доступ к getAction() для чтения типа действия. Получив событие перемещения, вы сопоставляете тип действия для выполнения соответствующих задач.

Обратите внимание, что все ветви в выражении when возвращают логическое значение true, за исключением ветви else.

Затем добавьте в onCreate() следующее:

binding.maskDropArea.setOnDragListener(maskDragListener)

Вы берете ссылку на view области перемещения маски, который будет реагировать на события перемещения. Затем вы передаете слушатель событий перемещения, maskDragListener, в setOnDragListener().

 

Обработка событий во время перемещения

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

Добавьте следующие фрагменты кода в свой maskDragListener.

DragEvent.ACTION_DRAG_ENTERED -> {
  binding.maskDropArea.alpha = 0.3f
  true
}

Приведенный выше код затемняет view области перемещения, когда маска входит в границы области перемещения. Это указывает пользователю, что маска находится внутри области перемещения.

DragEvent.ACTION_DRAG_EXITED -> {
  binding.maskDropArea.alpha = 1.0f
  draggableItem.visibility = View.VISIBLE
  view.invalidate()
  true
}

Этот код сбрасывает прозрачность вью области перемещения на 1.0f и сбрасывает видимость view маски на VISIBLE, когда он выходит за границы области перемещения. Это визуально указывает на то, что перемещение за пределы после отпускания не работает.

Теперь вы готовы позволить пользователю отпустить маску.

 

Обработка операции Drop

Когда пользователь отпускает маску drag shadow, системе необходимо отправить событие перемещения с типом действия ACTION_DROP в представление слушателя. Чтобы реализовать это, добавьте следующий фрагмент кода в свой maskDragListener.

DragEvent.ACTION_DROP -> {
  //1
  binding.maskDropArea.alpha = 1.0f
  //2
  if (dragEvent.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
    val draggedData = dragEvent.clipData.getItemAt(0).text
    //TODO : perform any action on the draggedData
  }
  //3
  true
}

Приведенный выше код выполняет следующие действия с слушателем событий перемещения:

1. Сбрасывает прозрачность вью области перемещения на 1.0f, когда пользователь отпускает маску.
2. Дополнительно считывает данные из ClipData через getClipData().
3. После успешного завершения обработки возвращает логическое значение true или логическое значение false. ACTION_DRAG_ENDED вернет это значение при вызове getResult().

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

 

Ответ на события окончания перемещения

Когда пользователь отпускает drag shadow, система отправляет событие перемещения всем зарегистрированным слушателям событий перемещения с типом действия ACTION_DRAG_ENDED.

Получение ACTION_DRAG_ENDED означает конец операции перемещения.

 

Настройка видимости view

Сразу после отправки ACTION_DROP, системе необходимо отправить ACTION_DRAG_ENDED. Добавьте следующий фрагмент кода в свой maskDragListener, чтобы установить видимость view маски:

DragEvent.ACTION_DRAG_ENDED -> {
  draggableItem.visibility = View.VISIBLE
  view.invalidate()
  true
}

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

Затем вы увидите, как переместить объект в новое положение, когда пользователь его отпустит.

 

Перемещение view в новое положение

Когда пользователь опускает перемещаемый view маски в область перемещения, вам необходимо разместить маску в месте перемещения. Это твоя следующая цель.

Получение положения по оси X и Y

Вы начнете с получения координат X и Y места выхода drag shadow из события перемещения. Это позволяет расположить маску в области перемещения. Для этого вызовите следующий код из реализации слушателя событий перемещения для события ACTION_DROP:

dragEvent.x
dragEvent.y

Чтобы получить координаты x, y из объекта события перемещения, используйте методы getX() и getY(). Координаты x, y из события перемещения соответствуют последней позиции drag shadow маски перед тем, как пользователь отпустил ее.

 

Обновление позиции перемещаемого вью

В Android каждое view на холсте начинает вычислять свою позицию с левого верхнего угла и заканчивается в правом нижнем углу.
Если вы используете координаты x, y из события перемещения для размещения маски, она будет привязана к координатам левого верхнего угла.

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

Создайте ссылку на view перемещаемой маски:

val draggableItem = dragEvent.localState as View.

Чтобы обновить координаты x, y view перемещаемой маски, сначала импортируйте androidx.constraintlayout.widget.ConstraintLayout. Затем добавьте следующий фрагмент кода в ветку DragEvent.ACTION_DROP вашего maskDragListener:

//1
draggableItem.x = dragEvent.x - (draggableItem.width / 2)
//2
draggableItem.y = dragEvent.y - (draggableItem.height / 2)

//3
val parent = draggableItem.parent as ConstraintLayout
//4
parent.removeView(draggableItem)

//5
val dropArea = view as ConstraintLayout
//6
dropArea.addView(draggableItem)
//7
true

Этот код выравнивает центр маски с последней точкой касания перед перемещением. Он также удаляет маску из ее предыдущего местоположения и добавляет ее в новое местоположение. Проходя его пошагово, вы:

1. Переместите маску с обновленной координатой x. Вычтите половину ширины view маски из координаты x события перемещения. Затем присвоите разницу значений координате x объекта draggableItem.
2. Переместите маску с обновленной координатой y. Вычтите половину высоты вью маски из координаты y события перемещения. Присвоите разницу в значении координате y объекта draggableItem.
3. Создадите ссылку на родительскую группу viewGroup маски.
4. Удалите маску из родительской viewGroup.
5. Создадите ссылку на новую viewGroup, область перемещения маски.
6. Добавите view маски в эту новую viewGroup.
7. Вернёте логическое значение true, чтобы указать, что операция удаления завершилась успешно.

На данный момент операция перемещения работает, но вам нужно сделать одно последнее улучшение.

 

Определение наличия маски на лице

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

Вызов нового метода checkIfMaskIsOnFace(dragEvent: DragEvent) в ветке DragEvent.ACTION_DROP maskDragListener непосредственно перед возвратом значения true:

DragEvent.ACTION_DROP -> {
  ...
  checkIfMaskIsOnFace(dragEvent)
  true
}

В checkIfMaskIsOnFace() вы создадите ссылку на view лица, талисман Bugdroid, чтобы измерить границы view. Если координаты x и y события перемещения находятся в пределах границ view лица, тогда маска находится на лице.

Затем добавьте следующие переменные в MainActivity.kt:

private val maskOn = "Bingo! Mask On"
private val maskOff = "Mask off"

Они будут служить текстовыми значениями для всплывающего сообщения, которое создаст checkIfMaskIsOnFace().

Пришло время создать checkIfMaskIsOnFace(dragEvent: DragEvent). Начните с импорта android.widget.Toast.

Реализуйте checkIfMaskIsOnFace(dragEvent: DragEvent) в MainActivity.kt для отображения соответствующего всплывающего сообщения:

private fun checkIfMaskIsOnFace(dragEvent: DragEvent) {
  //1
  val faceXStart = binding.faceArea.x
  val faceYStart = binding.faceArea.y

  //2
  val faceXEnd = faceXStart + binding.faceArea.width
  val faceYEnd = faceYStart + binding.faceArea.height
  //3
  val toastMsg = if (dragEvent.x in faceXStart..faceXEnd && dragEvent.y in faceYStart..faceYEnd){
    maskOn
  } else {
    maskOff
  }
  //4
  Toast.makeText(this, toastMsg, Toast.LENGTH_SHORT).show()
}

Вот что делает приведенный выше код:

1. Определяет координаты x, y левой верхней точки талисмана Bugdroid.
2. Добавляет ширину и высоту грани к левой верхней точке грани для вычисления координат x, y нижней конечной точки.
3. Проверяет, находится ли место падения маски в пределах грани лица. Это позволяет вам установить соответствующее тост-сообщение (toast message).
4. Отображает всплывающее сообщение, указывающее, находится ли маска на лице или нет.

Выполните сборку и запустите. Вы увидите всплывающее сообщение о том, надета ли маска на лицо или нет.

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

Оригинал статьи

Содержание