/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.remoteauth.introduction import android.content.Context import android.util.AttributeSet import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.ViewPager2 import com.airbnb.lottie.LottieAnimationView import com.android.settings.R import com.android.settingslib.widget.LottieColorUtils class IntroductionImageCarousel : ConstraintLayout { private val carousel: ViewPager2 by lazy { requireViewById<ViewPager2>(R.id.image_carousel) } private val progressIndicator: RecyclerView by lazy { requireViewById<RecyclerView>(R.id.carousel_progress_indicator) } private val backArrow: ImageView by lazy { requireViewById<ImageView>(R.id.carousel_back_arrow) } private val forwardArrow: ImageView by lazy { requireViewById<ImageView>(R.id.carousel_forward_arrow) } private val progressIndicatorAdapter = ProgressIndicatorAdapter() // The index of the current animation we are on private var currentPage = 0 set(value) { val pageRange = 0..(ANIMATION_LIST.size - 1) field = value.coerceIn(pageRange) backArrow.isEnabled = field > pageRange.start forwardArrow.isEnabled = field < pageRange.endInclusive carousel.setCurrentItem(field) progressIndicatorAdapter.currentIndex = field } private val onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { currentPage = position } } constructor(context: Context) : super(context) constructor(context: Context, attrSet: AttributeSet?) : super(context, attrSet) init { LayoutInflater.from(context).inflate(R.layout.remote_auth_introduction_image_carousel, this) with(carousel) { setPageTransformer( MarginPageTransformer( context.resources.getDimension(R.dimen.remoteauth_introduction_fragment_padding_horizontal).toInt() ) ) adapter = ImageCarouselAdapter() registerOnPageChangeCallback(onPageChangeCallback) } with(progressIndicator) { layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) adapter = progressIndicatorAdapter } backArrow.setOnClickListener { currentPage-- } forwardArrow.setOnClickListener { currentPage++ } } fun unregister() { carousel.unregisterOnPageChangeCallback(onPageChangeCallback) } private class AnimationViewHolder(val context: Context, itemView: View) : RecyclerView.ViewHolder(itemView) { val animationView = itemView.requireViewById<LottieAnimationView>(R.id.explanation_animation) val descriptionText = itemView.requireViewById<TextView>(R.id.carousel_text) } /** Adapter for the onboarding animations. */ private class ImageCarouselAdapter : RecyclerView.Adapter<AnimationViewHolder>() { override fun getItemCount() = ANIMATION_LIST.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AnimationViewHolder(parent.context, LayoutInflater.from(parent.context).inflate(R.layout.remote_auth_introduction_image_carousel_item, parent, false)) override fun onBindViewHolder(holder: AnimationViewHolder, position: Int) { with(holder.animationView) { setAnimation(ANIMATION_LIST[position].first) LottieColorUtils.applyDynamicColors(holder.context, this) } holder.descriptionText.setText(ANIMATION_LIST[position].second) with(holder.itemView) { // This makes sure that the proper description text instead of a generic "Page" label is // verbalized by Talkback when switching to a new page on the ViewPager2. contentDescription = context.getString(ANIMATION_LIST[position].second) } } } /** Adapter for icons indicating carousel progress. */ private class ProgressIndicatorAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var currentIndex: Int = 0 set(value) { val previousIndex = field field = value.coerceIn(0, getItemCount() - 1) notifyItemChanged(previousIndex) notifyItemChanged(field) } override fun getItemCount() = ANIMATION_LIST.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = object : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.remote_auth_introduction_image_carousel_progress_icon, parent, false)) {} override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { holder.itemView.isSelected = position == currentIndex } } companion object { @VisibleForTesting val ANIMATION_LIST = listOf( Pair( R.raw.remoteauth_explanation_swipe_animation, R.string.security_settings_remoteauth_enroll_introduction_animation_swipe_up ), Pair( R.raw.remoteauth_explanation_notification_animation, R.string.security_settings_remoteauth_enroll_introduction_animation_tap_notification ), ) const val TAG = "RemoteAuthCarousel" } }