Convertir ViewPager en un administrador de fragmentos con animaciones estilo iOS

Muchos desarrolladores para Android enfrentaron el problema de implementar animaciones y transiciones al abrir nuevos fragmentos. Se nos invita a usar ya sea agregando fragmentos al contenedor, superponiéndolos uno encima del otro, o reproduciéndolos (reemplazando un fragmento por otro). La repetición tiene cuatro tipos de animaciones:


Vive todo se parece a esto:
.beginTransaction() .setCustomAnimations( R.anim.enter_from_left, //   2 R.anim.exit_to_right, //   1 R.anim.enter_from_right, //   1 R.anim.exit_to_left) //   2 .replace(R.id.container, myFragment) .commit() 

imagen


El problema con las repeticiones es que a) el fragmento anterior se destruye, b) no hay forma de establecer una acción para cerrar el fragmento con un gesto (por ejemplo, como se implementa en Google Inbox).


Agregar fragmentos a la pila (agregar) le permite usar animaciones solo para que el fragmento se abra, la parte posterior estará inmóvil.


Y todo esto, por supuesto, va acompañado de una representación pobre y marcos rotos.


Como resultado, incluso las aplicaciones grandes como VKontakte o Instagram no usan fragmentos de animaciones en absoluto en sus aplicaciones.


Hace un año y medio, Telegram presentó Telegram x (una versión de prueba de su cliente). Resolvieron este problema así:


imagen


Aquí, se implementa la animación de los fragmentos delantero y trasero, así como la capacidad de cerrar fragmentos con un gesto.


Logré hacer algo similar y me gustaría compartir mi método de abrir fragmentos:


imagen


Entonces, cree la clase NavigatorViewPager:


 class NavigatorViewPager : ViewPager { init { init() } constructor(context: Context) : super(context) constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) override fun canScrollHorizontally(direction: Int): Boolean { return false } //  private fun init() { // PageTransformer       setPageTransformer(false, NavigatorPageTransformer()) //   overScrollMode = View.OVER_SCROLL_NEVER //       , //    setDurationScroll(300) } //      fun setDurationScroll(millis: Int) { try { val viewpager = ViewPager::class.java val scroller = viewpager.getDeclaredField("mScroller") scroller.isAccessible = true scroller.set(this, OwnScroller(context, millis)) } catch (e: Exception) { e.printStackTrace() } } //      DecelerateInterpolator() inner class OwnScroller(context: Context, durationScroll: Int) : Scroller(context, DecelerateInterpolator(1.5f)) { private var durationScrollMillis = 1 init { this.durationScrollMillis = durationScroll } override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) { super.startScroll(startX, startY, dx, dy, durationScrollMillis) } } } 

Ahora tenemos nuestro navegador, que usamos como contenedor para todos los fragmentos de nuestra actividad:


 <info.yamm.project2.navigator.NavigatorViewPager xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/navigator_view_pager" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black" android:fitsSystemWindows="false" tools:context=".activities.MainActivity"/> 

Fondo conjunto negro. Esto es necesario para simular la sombra en el fragmento cerrado. Además será más claro.


Ahora necesitamos un adaptador en el que colocaremos los fragmentos:


 class NavigatorAdapter(val fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) { // ArrayList      private val listOfFragments: ArrayList<BaseFragment> = ArrayList() //  fun addFragment(fragment: BaseFragment) { listOfFragments.add(fragment) notifyDataSetChanged() } //   fun removeLastFragment() { listOfFragments.removeAt(listOfFragments.size - 1) notifyDataSetChanged() } //     fun getFragmentsCount(): Int { return listOfFragments.size } override fun getItemPosition(`object`: Any): Int { val index = listOfFragments.indexOf(`object`) //        return if (index == -1) PagerAdapter.POSITION_NONE else index } override fun getItem(position: Int): Fragment? { return listOfFragments[position] } override fun getCount(): Int { return listOfFragments.size } override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { super.destroyItem(container, position, `object`) } } 

Inmediatamente cree un transformador para nuestro navegador:


 class NavigatorPageTransformer : ViewPager.PageTransformer { override fun transformPage(view: View, position: Float) { // PageTransformer       view.apply { val pageWidth = width when { //        position <= -1 -> { //   INVISIBLE       //    visibility = View.INVISIBLE } // ,          position > 0 && position <= 1 -> { alpha = 1f visibility = View.VISIBLE translationX = 0f } //         // (    ,     NavigatorViewPager?) position <= 0 -> { alpha = 1.0F - Math.abs(position) / 2 translationX = -pageWidth * position / 1.3F visibility = View.VISIBLE } //    ,     else -> { visibility = View.INVISIBLE } } } } } 

Ahora, ¡lo más interesante! Prescribimos las acciones necesarias para abrir fragmentos en nuestra Actividad:


 class MainActivity : BaseActivity() { private lateinit var navigatorAdapter: NavigatorAdapter private lateinit var navigatorViewPager: NavigatorViewPager private lateinit var mainFragment: MainFragment override fun onCreate(savedInstanceState: Bundle?) { setTheme(info.yamm.project2.R.style.AppTheme) //   navigatorViewPager = findViewById<NavigatorViewPager>(info.yamm.project2.R.id.navigator_view_pager) //   (    BottomNavigationView   ) mainFragment = MainFragment() //  navigatorAdapter = NavigatorAdapter(supportFragmentManager) //       addFragment(mainFragment) //     navigatorViewPager.adapter = navigatorAdapter //      // :   FragmentStatePagerAdapter,   //     . // FragmentPagerAdapter   ,         // ,    . // ,     INVISIBLE  PageTransformer //     ,        //   .    , //       navigatorViewPager.offscreenPageLimit = 30 var canRemoveFragment: Boolean = false //         var sumPositionAndPositionOffset = 0.0f //   navigatorViewPager.addOnPageChangeListener(object : OnPageChangeListener { //     ,     override fun onPageScrollStateChanged(state: Int) { if (state == 0 && canRemoveFragment) { while ((navigatorAdapter.getFragmentsCount() - 1) > navigatorViewPager.currentItem) { navigatorAdapter.removeLastFragment() } } } //       //        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { canRemoveFragment = position + positionOffset < sumPositionAndPositionOffset sumPositionAndPositionOffset = position + positionOffset } override fun onPageSelected(position: Int) { } }) } //             fun addFragment(fragment: BaseFragment) { navigatorAdapter.addFragment(fragment) navigatorViewPager.currentItem = navigatorViewPager.currentItem + 1 } //    "" override fun onBackPressed() { if (navigatorAdapter.getFragmentsCount() > 1) { navigatorViewPager.setCurrentItem(navigatorViewPager.currentItem - 1, true) } else { finish() } } } 

Eso, de hecho, es todo. Ahora en cualquier fragmento llamamos al método de Activiti:


 (activity as MainActivity).addFragment(ConversationFragment()) 

Y al deslizar hacia la derecha, él mismo será eliminado de la pila con la ayuda de nuestro oyente OnPageChangeListener.


Este método no es ideal, tal vez se abrirán algunas dificultades en el desarrollo posterior, pero aún no he encontrado ningún problema de usabilidad, tal vez los desarrolladores experimentados me corrijan o me digan algo. Puedes ver cómo funciona todo en un ejemplo real aquí .

Source: https://habr.com/ru/post/453166/


All Articles