Привет, Хабр! Меня зовут Павел Беловол, я Android-разработчик на проекте онлайн-кинотеатра KION в МТС Digital. Это новая часть сериала о внедрении фичи Autoplay в KION, в которой я расскажу про свой личный опыт работы с MotionLayout на примере продакшн-задачи в KION. Из этой статьи вы узнаете, где нужно использовать MotionLayout, а где лучше обойтись без него и писать код анимации самостоятельно.

Небольшая вводная:

MotionLayout – это контейнер, который позволяет просто создавать сложные анимации, для чего требуется лишь описать сцену. Более подробно о MotionLayout вы можете почитать тут.

А теперь поговорим о нашей фиче.

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

Мы в KION подумали, что по окончании просмотра фильма было бы здорово предложить зрителям похожий контент с хорошим рейтингом. Именно тот, который наилучшим образом подходит конкретному зрителю.

Обсудив эту задачу, мы пришли к выводу, что нам нужно следующее:

  • api, которое вернет похожий фильм;

  • доработка на клиенте, которая будет предлагать пользователю фильм, полученный от api.

Я не буду раскрывать подробности реализации api, это тема для отдельной статьи. Сейчас просто примем во внимание, что аpi у нас функционирует и находит лучший подобный фильм.

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

Анимация. Желаемый результат
Анимация. Желаемый результат

О том, что пользователь досмотрел фильм до финальных титров, мы узнаем от api. И есть слушатель в коде, который сообщит, что настала пора играть анимацию.

Немного расскажу про кнопки. При нажатии на «Смотреть титры» пользователя должно вернуть назад к просмотру фильма.

Возврат пользователя к просмотру фильма по нажатию кнопки «Смотреть титры»
Возврат пользователя к просмотру фильма по нажатию кнопки «Смотреть титры»

По нажатию на маленькое окошко с плеером нужно выполнить возврат к просмотру титров, то есть это действие равносильно нажатию на кнопку «Смотреть титры».

По нажатию на кнопку «Следующий фильм» или по окончании обратного отсчета включится следующий похожий фильм.

По нажатию на кнопку «X» выполняется выход из экрана с плеером. 

С кликами разобрались, теперь декомпозируем анимацию:

  1. Текущее окошко плеера уменьшается и перемещается в левый нижний угол;

  2. В качестве фона устанавливается постер следующего похожего фильма;

  3. Появляется кнопка «Х» в правом верхнем углу;

  4. Снизу выезжает блок с названием, жанром и кнопками «Смотреть титры» и «Следующий фильм»;

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

Пункты 1-4 мы будем анимировать полностью с помощью MotionLayout, пункт 5 будем анимировать частично с помощью MotionLayout, частично – вручную. Чуть позже объясню, почему так.

От теории к практике:

Начнем с самого простого, создадим xml-файл activity_main.xml с разметкой наших виджетов.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 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:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/scene"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/poster"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        app:srcCompat="@drawable/poster"
        tools:ignore="ContentDescription" />

    <View
        android:id="@+id/shadow"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:background="#99000000"
        tools:ignore="ContentDescription" />

    <ImageView
        android:id="@+id/close"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="24dp"
        android:padding="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_close_24"
        tools:ignore="ContentDescription,ImageContrastCheck" />

    <TextView
        android:id="@+id/filmTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="12dp"
        android:gravity="end"
        android:text="@string/vod_title"
        android:textColor="#EEEEEE"
        android:textSize="24sp"
        app:layout_constraintBottom_toTopOf="@id/filmInfo"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/player"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="1.0"
        app:layout_constraintVertical_chainStyle="packed" />


    <TextView
        android:id="@+id/filmInfo"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="32dp"
        android:gravity="end"
        android:text="@string/vod_detail"
        android:textColor="#B2C6DB"
        android:textSize="14sp"
        app:layout_constraintBottom_toTopOf="@id/nextFilm"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/player"
        app:layout_constraintTop_toBottomOf="@+id/filmTitle"
        app:layout_constraintVertical_chainStyle="packed" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/watchCredits"
        android:layout_width="wrap_content"
        android:layout_height="44dp"
        android:layout_marginEnd="10dp"
        android:alpha="0.8"
        android:background="@drawable/bg_credits"
        android:gravity="center"
        android:paddingLeft="20dp"
        android:paddingTop="13dp"
        android:paddingRight="20dp"
        android:paddingBottom="13dp"
        android:text="@string/watch_credits"
        android:textAllCaps="false"
        android:textColor="@android:color/white"
        android:textSize="12sp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="@+id/nextFilm"
        app:layout_constraintEnd_toStartOf="@id/nextFilm"
        app:layout_constraintTop_toTopOf="@id/nextFilm"
        app:lineSpacing="2sp"
        tools:ignore="TouchTargetSizeCheck" />

    <com.example.motionlayoutsample.ProgressButton
        android:id="@+id/nextFilm"
        android:layout_width="202dp"
        android:layout_height="44dp"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="80dp"
        android:paddingLeft="20dp"
        android:paddingTop="13dp"
        android:paddingRight="20dp"
        android:paddingBottom="13dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/filmInfo"
        app:text="@string/next_film"
        tools:ignore="TouchTargetSizeCheck" />

    <ImageView
        android:id="@+id/player"
        android:layout_width="257dp"
        android:layout_height="140dp"
        android:src="@drawable/content"
        android:padding="2dp"
        android:layout_marginStart="24dp"
        android:layout_marginBottom="80dp"
        android:background="@drawable/bg_player"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:ignore="ContentDescription" />

</androidx.constraintlayout.motion.widget.MotionLayout>

Краткое пояснение по идентификаторам :

  • motionLayout – контейнер выполняющий анимацию

  • poster – постер следующего фильма

  • shadow – затемнение

  • close – иконка «X»

  • filmTitle – текст с названием фильма

  • filmInfo – текст с описанием фильма

  • watchCredits – кнопка с названием «Смотреть титры»

  • nextFilm – кнопка с названием «Следующий фильм»

  • player – картинка с имитацией воспроизведения плеера (в продакшн-коде вместо imageView, как правило, контейнер, в который добавляется плеер. Например, FrameLayout).

Также создадим файл сцены scene.xml, именно в нем мы будем описывать, как нужно анимировать виджеты.

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/start">
        
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">

       
    </ConstraintSet>

    <Transition
        android:id="@+id/transition"
        app:duration="500"
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start" />

</MotionScene>

Краткое пояснение по идентификаторам :

  • start – начальное состояние анимации;

  • end – конечное состояние анимации;

  • transition – переход между состояниями start и end, параметр duration (длительность анимации) установим в 500 миллисекунд.

Результат в Android Studio
Результат в Android Studio

Если мы попытаемся проиграть сцену в Android Studio, то ничего не произойдет, так как наши constraintSet пустые, и нет никаких условий анимации. Давайте исправим это.

Полный файл scene.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
  
    <ConstraintSet android:id="@+id/start">

        <Constraint
            android:id="@+id/player"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        <Constraint
            android:id="@+id/filmTitle"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="24dp"
            app:layout_constraintVertical_chainStyle="packed"
            android:layout_marginBottom="12dp"
            app:layout_constraintBottom_toTopOf="@id/filmInfo"
            app:layout_constraintVertical_bias="1.0"
            app:layout_constraintTop_toBottomOf="parent" />
        <Constraint
            android:layout_marginEnd="24dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="32dp"
            app:layout_constraintBottom_toTopOf="@id/nextFilm"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/filmTitle"
            app:layout_constraintVertical_chainStyle="packed"
            android:id="@+id/filmInfo" />
        <Constraint
            android:id="@+id/shadow"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:visibility="invisible" />
        <Constraint
            android:id="@+id/close"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="28dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="28dp"
            android:visibility="invisible" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">

        <Constraint
            android:id="@+id/poster"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        <Constraint
            android:id="@+id/shadow"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:visibility="visible" />
        <Constraint
            android:id="@+id/close"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="28dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="28dp"
            android:visibility="visible" />
        <Constraint
            android:id="@+id/filmTitle"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="24dp"
            android:layout_marginStart="24dp"
            app:layout_constraintVertical_chainStyle="packed"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toEndOf="@+id/player"
            android:layout_marginBottom="12dp"
            app:layout_constraintBottom_toTopOf="@id/filmInfo"
            app:layout_constraintVertical_bias="1.0" />
        <Constraint
            android:id="@+id/filmInfo"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="24dp"
            android:layout_marginStart="24dp"
            app:layout_constraintVertical_chainStyle="packed"
            app:layout_constraintStart_toEndOf="@+id/player"
            android:layout_marginBottom="32dp"
            app:layout_constraintBottom_toTopOf="@id/nextFilm"
            app:layout_constraintTop_toBottomOf="@+id/filmTitle" />
        <Constraint
            android:id="@+id/watchCredits"
            android:layout_width="wrap_content"
            android:layout_height="44dp"
            android:visibility="visible"
            android:layout_marginEnd="10dp"
            app:layout_constraintTop_toTopOf="@id/nextFilm"
            app:layout_constraintEnd_toStartOf="@id/nextFilm"
            app:layout_constraintBottom_toBottomOf="@+id/nextFilm"
            android:alpha="0.8" />
        <Constraint
            android:id="@+id/nextFilm"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="202dp"
            android:layout_height="44dp"
            android:layout_marginEnd="24dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="80dp"
            app:layout_constraintTop_toBottomOf="@id/filmInfo" />
        <Constraint
            android:id="@+id/player"
            android:layout_width="257dp"
            android:layout_height="140dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="80dp"
            android:layout_marginStart="24dp"
            app:layout_constraintStart_toStartOf="parent" />
    </ConstraintSet>

    <Transition
        android:id="@+id/transition"
        app:duration="500"
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start" />

</MotionScene>

В ConstraintSet с идентификатором start мы описали свойства виджетов в начальном состоянии анимации, а с идентификатором end, – свойства виджетов в конечном состоянии анимации.

Результат воспроизведения в Android studio на скорости 0.25x (чтобы плавнее увидеть переход)
Результат воспроизведения в Android studio на скорости 0.25x (чтобы плавнее увидеть переход)

Задача практически выполнена, но мы забыли про одну вещь — кнопка «Следующая серия» должна анимироваться, наполняясь индикатором прогресса. Если до этого мы анимировали элементы с помощью MotionLayout, то в этот раз мы должны поступить иначе, так как кнопка с прогрессом — кастомный элемент, и у нас не получится стандартными атрибутами сцены выполнить анимацию заполнения прогресса.

В этом случае нам придется написать код анимации самостоятельно.

Код кнопки
import android.animation.ValueAnimator
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.animation.addListener
import androidx.core.content.ContextCompat
import androidx.core.content.res.use

class ProgressButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : CardView(context, attrs, defStyleAttr) {

    var onCountDown: (() -> Unit)? = null
    private var clickListener: OnClickListener? = null

    private val textView = TextView(context).apply {
        gravity = Gravity.CENTER
        setTextColor(
            ContextCompat.getColor(context, R.color.progress_button_text_color)
        )
        isAllCaps = false
        layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
            setPadding(0, 0, 12.toPx, 0)
        }
    }
    private val imageView = ImageView(context).apply {
        setImageResource(R.drawable.ic_player_next)
        layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
    }

    private val progressBar =
        ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal).apply {
            max = 100
            progress = if (isInEditMode) 70 else 0
            isIndeterminate = false
            layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
            progressDrawable = ContextCompat.getDrawable(
                context, R.drawable.next_episode_progress_bar_states
            )
        }


    private val animator = ValueAnimator.ofInt(0, 100).apply {
        addUpdateListener {
            progressBar.progress = it.animatedValue as Int
        }
        addListener(
            onEnd = {
                if (animatedValue == 100) {
                    onCountDown?.invoke()
                }
            }
        )
        duration = 7000
    }

    fun startProgress() {
        if (!animator.isRunning) {
            animator.start()
        }
    }

    fun cancelProgress() {
        animator.cancel()
    }

    init {
        addView(progressBar)
        addView(
            LinearLayout(context).apply {
                orientation = LinearLayout.HORIZONTAL
                layoutParams =
                    LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
                        gravity = Gravity.CENTER
                    }
                gravity = Gravity.CENTER
                addView(textView)
                addView(imageView)
            }
        )
        radius = 7.toPx.toFloat()
        super.setOnClickListener {
            animator.cancel()
            clickListener?.onClick(it)
        }
        setBackgroundResource(R.drawable.next_episode_button_stroke)
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.PlayerNextEpisodeButton,
            0, 0
        ).use {
            textView.text = it.getString(R.styleable.PlayerNextEpisodeButton_text)
        }
        isSaveEnabled = true
    }

    override fun setOnClickListener(l: OnClickListener?) {
        clickListener = l
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        cancelProgress()
        onCountDown = null
    }

    override fun onSaveInstanceState(): Parcelable? {
        val superState = super.onSaveInstanceState()
        superState?.let {
            val state = SavedState(superState)
            state.progressBarState = progressBar.onSaveInstanceState()
            state.isProgressRunning = animator.isRunning
            state.currentPlayTime = animator.currentPlayTime
            return state
        } ?: run {
            return superState
        }
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        when (state) {
            is SavedState -> {
                super.onRestoreInstanceState(state.superState)
                progressBar.onRestoreInstanceState(state.progressBarState)
                if (state.isProgressRunning) {
                    animator.currentPlayTime = state.currentPlayTime
                    startProgress()
                }
            }
            else -> {
                super.onRestoreInstanceState(state)
            }
        }
    }

    private class SavedState : BaseSavedState {
        var progressBarState: Parcelable? = null
        var isProgressRunning = false
        var currentPlayTime = 0L

        constructor(parcel: Parcel) : super(parcel) {
            progressBarState = parcel.readParcelable(ProgressBar::class.java.classLoader)
            isProgressRunning = parcel.readInt() == 1
            currentPlayTime = parcel.readLong()
        }

        constructor (parcelable: Parcelable?) : super(parcelable)

        override fun writeToParcel(out: Parcel?, flags: Int) {
            super.writeToParcel(out, flags)
            out?.writeParcelable(progressBarState, flags)
            out?.writeInt(if (isProgressRunning) 1 else 0)
            out?.writeLong(currentPlayTime)
        }

        companion object {
            @Suppress("unused")
            @JvmField
            val CREATOR = object : Parcelable.Creator<SavedState> {
                override fun createFromParcel(source: Parcel): SavedState {
                    return SavedState(source)
                }

                override fun newArray(size: Int): Array<SavedState?> {
                    return arrayOfNulls(size)
                }
            }
        }
    }

}

Мы написали код кнопки с прогрессом, в качестве анимирования использовали стандартный ValueAnimator, учитывая сохранение состояния после смены конфигурации.

Ключевые методы:

  • startProgress() – запускает анимацию заполнения прогресса

  • cancelProgress() – останавливает анимацию заполнения прогресса

Результат на реальном устройстве
Результат на реальном устройстве

Теперь остается самая важная задача — запустить общую анимацию. 

Тут все просто.

motionLayout.transitionToEnd() – запускает анимацию начиная с состояния start к end

motionLayout.transitionToStart() – запускает анимацию начиная с состояния end к start

Теперь добавим код запуска к слушателям кнопок.

close.setOnClickListener {
    nextFilm.cancelProgress()
}
player.setOnClickListener {
    motionLayout.transitionToStart()
    nextFilm.cancelProgress()
}
watchCredits.setOnClickListener {
    motionLayout.transitionToStart()
    nextFilm.cancelProgress()
}
nextFilm.setOnClickListener {
    motionLayout.transitionToStart()
    nextFilm.cancelProgress()
}
nextFilm.onCountDown = {
    nextFilm.performClick()
}

Так как кнопка progressButton – это кастомный элемент, который анимируется частично самостоятельно, нужно отдельно вызывать startProgress(), cancelProgress().

И добавим код запуска анимации по достижению титров.

nextFilm.startProgress()

motionLayout.transitionToEnd()

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

Исправим это:

class MainViewModel : ViewModel() {
    var motionLayoutState : Bundle? = null
}

class MainActivity : AppCompatActivity() {

    private val viewModel by viewModels<MainViewModel>()
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel.motionLayoutState?.let {
            binding.motionLayout.transitionState = it
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        viewModel.motionLayoutState = binding.motionLayout.transitionState
        super.onSaveInstanceState(outState)
    }
}

Мы сохранили состояние MotionLayout с помощью viewModel, теперь сцена до и после поворота отображается корректно. Напомню, что progressButton уже содержит в себе код сохранения и восстановления состояния.

Финальный результат на реальном устройстве:

Каков итог?

MotionLayout – мощный и удобный инструмент для написания анимаций на Android, но не всегда и не все получится анимировать c его помощью. Иногда придется писать код анимации самостоятельно, как это сделал я для кнопки с заполнением прогресса.

Спасибо за уделенное время! Если у вас есть вопросы, замечания или истории из личного опыта работы с MotionLayout – добро пожаловать в комментарии.

Мои коллеги с различных платформ написали свои статьи по этой фиче, советую также ознакомиться с их трудами:

Про саму фичу Autoplay в онлайн-кинотеатре

Про нюансы реализации фичи на tvOS

Про разработку фичи на Angular под SmartTV

А еще мы рассказывали на Хабре про другую фичу KION, реализованную с помощью искусственного интеллекта – пропуск титров.

Подробно про саму фичу

Про проблемы и их решения с помощью Computed Properties в Angular

Комментарии (13)


  1. belovol Автор
    15.01.2023 00:15

    Расскажите чтобы вам интересно было узнать еще про motion layout. Какие моменты стоило бы подробнее рассмотреть. Учту для будущих статей. Свои предложения можете оставлять под этим комментарием, или написать мне в личку в телеграмм. Контакты на этой странице в конце поста ????


  1. HiroProtagonist
    12.01.2023 11:18
    +3

    Сорвался играть в comix zone после кдпв


    1. belovol Автор
      12.01.2023 21:03

      Спасибо, мы старались и знали что найдутся ценители :)


  1. VitallyCom
    12.01.2023 13:43
    +1

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


    1. belovol Автор
      12.01.2023 21:05

      Спасибо вам за комментарий и похвалу :)

      Понимаю вас, но есть люди кто по достоинству оценил фичу и нашел много крутого контента :)


      1. VitallyCom
        12.01.2023 21:09
        +2

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

        Тогда есть новая идея, сделать переключатель в настройках, включающий и отключающий «бесконечный» плей.


        1. belovol Автор
          13.01.2023 03:51

          Понял вас, заберу этот момент и обсужу внутри команды, спасибо за идею ????


  1. kavaynya
    13.01.2023 10:16
    +1

    Сколько не смотрю туториалы по MotionLayout, все время восторгаюсь, как же все классно выглядит и легко настраивается.
    Но как только пытаешься сам, что-то сделать, то выясняется что в AndroidStudio (Canary) MotionEditor не работает, нельзя просмотреть анимацию. А если и смог сделать, то она жутко тормозит. Пока одни негативные впечатления.


    1. belovol Автор
      13.01.2023 12:23

      Привет) спасибо за комментарий)

      Есть такое, не все идеально работает, постоянно приходится закрывать xml файл и открывать чтобы изменения увидеть в превью, бывает приходится ребилдить)

      Но пока что эти все танцы с бубном по времени быстрее, чем проверять на устройстве)


      1. kavaynya
        13.01.2023 18:14
        +1

        Хорошо, когда так. Но у меня простая анимация по скрытию одного элемента, тормозила ужасно на устройстве


        1. belovol Автор
          13.01.2023 19:38

          А что за устройство если не секрет?) у меня просто на слабеньком redmi 6A жуткие тормоза, а на redmi note 8t все плавно и красиво)


          1. kavaynya
            14.01.2023 08:22
            +1

            Не секрет. Realme 5 pro. Не новый, но и не слабый. В проекте где пытался его применить, уже используется MotionLayout на одном экране, тормозов при этом у себя я не наблюдаю, но работает немного странно


  1. belovol Автор
    14.01.2023 11:54

    Понял.

    Я бы на вашем месте обратил внимание на то как описана сцена. Может быть какие-то вещи избыточны или некорректны. А также обратил бы внимание на виджеты которые анимируете, может быть вложенность большая или элемент содержит логику из-за которой его отрисовка страдает. И еще бы посмотрел в сторону такого параметра motion:layoutDuringTransition

    Может он у вас задан, это может сильно влиять на перфоманс.


  1. belovol Автор
    15.01.2023 00:15

    Расскажите чтобы вам интересно было узнать еще про motion layout. Какие моменты стоило бы подробнее рассмотреть. Учту для будущих статей. Свои предложения можете оставлять под этим комментарием, или написать мне в личку в телеграмм. Контакты на этой странице в конце поста ????