Android解析自定义属性实现视差动画效果

前言

在学习本文之前,可以先了解下android LayoutInflater源码分析以及换肤框架实现原理,这里通过LayoutInflater解析自定义属性,实现对布局中的View进行平移操作。

最终效果

视差动画效果

实现思路

  • 首先自定义平移属性,如支持X轴方向进出平移:translationXIntranslationXOut、Y轴方向进出平移translationYIntranslationYOut
  • 由于我们在布局文件中使用自定义的属性,那我们对自定义的属性进行解析,那怎么才能解析我们自定义的属性呢?那就需要了解LayoutInflater源码,这里不再赘述,我们通过实现LayoutInflater.Factory2接口,重写onCreateView方法进行自定义属性解析,并把结果存放到View的自定义Tag中【避免与其他tag冲突】
  • 自定义属性解析完成后,我们便可以监听ViewPager滚动时根据设置的属性进行平移效果以完成View的视差平移;

相关代码

  • 自定义属性以及tag
  • attrs.xml
    <!--自定义视觉属性-->
   <!--X方向的位移-->
   <attr name="translationXIn" format="float"/>
   <attr name="translationXOut" format="float"/>
   <!--Y方向的位移-->
   <attr name="translationYIn" format="float"/>
   <attr name="translationYOut" format="float"/>
  • attrs.xml
    <item name="parallax_tag" type="id"/>
  • 布局文件中使用自定义属性
    fragment_page.xml
<?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:id="@+id/rootFirstPage"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:background="@android:color/holo_orange_dark">

   <ImageView
       android:id="@+id/ivFirstImage"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:src="@mipmap/s_0_1"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintHeight_percent="0.35"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       app:layout_constraintWidth_percent="0.5"
       app:translationXIn="0.4"
       app:translationXOut="0.4"
       app:translationYIn="0.4"
       app:translationYOut="0.4" />

   <ImageView
       android:id="@+id/ivSecondImage"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentEnd="true"
       android:layout_marginTop="100dp"
       android:layout_marginEnd="50dp"
       android:src="@mipmap/s_0_2"
       app:layout_constraintHeight_percent="0.1"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       app:layout_constraintWidth_percent="0.12"
       app:translationXIn="0.12"
       app:translationXOut="0.12"
       app:translationYIn="0.82"
       app:translationYOut="0.82" />

   <ImageView
       android:id="@+id/ivFourthImage"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginStart="60dp"
       android:layout_marginTop="120dp"
       android:src="@mipmap/s_0_4"
       app:layout_constraintHeight_percent="0.15"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       app:layout_constraintWidth_percent="0.12"
       app:translationXIn="0.2"
       app:translationXOut="0.2" />
</androidx.constraintlayout.widget.ConstraintLayout>
  • 自定义Fragment实现自定义属性解析
package com.crystal.view.parallax

import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import androidx.core.view.LayoutInflaterCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import com.crystal.view.R
import org.xmlpull.v1.XmlPullParser


/**
* 支持视差动画的自定义Fragment
* on 2022/11/14
*/
class ParallaxFragment : Fragment(), LayoutInflater.Factory2 {
   //用于fragment数据传递
   companion object {
       val LAYOUT_ID_KEY = "LAYOUT_ID_KEY"
   }

   //用于存放所有需要平移的View
   private val parallaxViews = arrayListOf<View>()

   //自定义的平移属性
   private val parallaxAttrs = intArrayOf(
       R.attr.translationXIn,
       R.attr.translationXOut,
       R.attr.translationYIn,
       R.attr.translationYOut
   )

   override fun onCreateView(
       inflater: LayoutInflater,
       container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View? {
       val layoutId = arguments?.getInt(LAYOUT_ID_KEY)
       //由于LayoutInflater是单例模式,这里我们首先需要构建一个新的inflater,如果直接传inflater,则代表所有的view的创建都会由此fragment去完成
       val newInflater = inflater.cloneInContext(activity)
       //设置走自己的onCreateView方法
       LayoutInflaterCompat.setFactory2(newInflater, this)
       return newInflater.inflate(layoutId ?: 0, container, false)
   }


   override fun onCreateView(
       parent: View,
       name: String,
       context: Context,
       attrs: AttributeSet
   ): View {
       Log.e("onCreateView", "our self onCreateView")
       val view = createView(parent, name, context, attrs)
       if (view != null) {
           analysisAttrs(view, context, attrs)
       }
       return view
   }

   /**
    * 解析自定义属性
    */
   private fun analysisAttrs(view: View, context: Context, attrs: AttributeSet) {
       val array = context.obtainStyledAttributes(attrs, parallaxAttrs)
       if (array != null && array.indexCount != 0) {
           val tag = ParallaxTag()
           for (i in 0 until array.indexCount) {
               when (val attr = array.getIndex(i)) {
                   0 -> {
                       tag.translationXIn = array.getFloat(attr, 0f)
                   }
                   1 -> {
                       tag.translationXOut = array.getFloat(attr, 0f)
                   }
                   2 -> {
                       tag.translationYIn = array.getFloat(attr, 0f)
                   }
                   3 -> {
                       tag.translationYOut = array.getFloat(attr, 0f)
                   }

               }
           }
           //给view设置一个自定义的tag
           view.setTag(R.id.parallax_tag, tag)
           parallaxViews.add(view)
           array.recycle()
       }

   }


   override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View {
       Log.e("onCreateView", "our self onCreateView")
       val view = createView(null, name, context, attrs)
       if (view != null) {
           analysisAttrs(view, context, attrs)
       }
       return view
   }

   private val IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21

   private fun createView(
       parent: View?,
       name: String,
       context: Context,
       attrs: AttributeSet
   ): View {
       //参考AppCompatDelegateImpl实现
       var inheritContext = false
       if (IS_PRE_LOLLIPOP) {
           inheritContext =
               if (attrs is XmlPullParser // If we have a XmlPullParser, we can detect where we are in the layout
               ) (attrs as XmlPullParser).depth > 1 // Otherwise we have to use the old heuristic
               else shouldInheritContext((parent as ViewParent?)!!)
       }
       val parallaxCompatViewInflater = ParallaxCompatViewInflater()
       return parallaxCompatViewInflater.createView(
           parent, name, context, attrs, inheritContext,
           IS_PRE_LOLLIPOP,  /* Only read android:theme pre-L (L+ handles this anyway) */
           true,  /* Read read app:theme as a fallback at all times for legacy reasons */
           false /* Only tint wrap the context if enabled */
       )
   }

   //参考AppCompatDelegateImpl实现
   private fun shouldInheritContext(parent: ViewParent): Boolean {
       var parent: ViewParent? = parent
           ?: // The initial parent is null so just return false
           return false
       val windowDecor: View = requireActivity().window.decorView
       while (true) {
           if (parent == null) {
               // Bingo. We've hit a view which has a null parent before being terminated from
               // the loop. This is (most probably) because it's the root view in an inflation
               // call, therefore we should inherit. This works as the inflated layout is only
               // added to the hierarchy at the end of the inflate() call.
               return true
           } else if (parent === windowDecor || parent !is View
               || ViewCompat.isAttachedToWindow((parent as View?)!!)
           ) {
               // We have either hit the window's decor view, a parent which isn't a View
               // (i.e. ViewRootImpl), or an attached view, so we know that the original parent
               // is currently added to the view hierarchy. This means that it has not be
               // inflated in the current inflate() call and we should not inherit the context.
               return false
           }
           parent = parent.getParent()
       }
   }

   fun getParallaxViews(): ArrayList<View> {
       return parallaxViews
   }
}

其中ParallaxTag为数据类

data class ParallaxTag(
    var translationXIn: Float = 0f,
    var translationXOut: Float = 0f,
    var translationYIn: Float = 0f,
    var translationYOut: Float = 0f
) {
    override fun toString(): String {
        return "translationXIn->$translationXIn translationXOut->$translationXOut translationYIn->$translationYIn translationYOut->$translationYOut";
    }
}

其中ParallaxCompatViewInflater类参考系统中的AppCompatViewInflater类,以调用createView方法,对应源码如下:

package com.crystal.view.parallax;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.InflateException;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.R;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.appcompat.widget.AppCompatCheckedTextView;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView;
import androidx.appcompat.widget.AppCompatRadioButton;
import androidx.appcompat.widget.AppCompatRatingBar;
import androidx.appcompat.widget.AppCompatSeekBar;
import androidx.appcompat.widget.AppCompatSpinner;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.appcompat.widget.AppCompatToggleButton;
import androidx.collection.SimpleArrayMap;
import androidx.core.view.ViewCompat;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* 参考AppCompatViewInflater类,以调用createView方法
* on 2022/11/14
*/
public class ParallaxCompatViewInflater {

   private static final Class<?>[] sConstructorSignature = new Class<?>[]{
           Context.class, AttributeSet.class};
   private static final int[] sOnClickAttrs = new int[]{android.R.attr.onClick};

   private static final String[] sClassPrefixList = {
           "android.widget.",
           "android.view.",
           "android.webkit."
   };

   private static final String LOG_TAG = "AppCompatViewInflater";

   private static final SimpleArrayMap<String, Constructor<? extends View>> sConstructorMap =
           new SimpleArrayMap<>();

   private final Object[] mConstructorArgs = new Object[2];

   final View createView(View parent, final String name, @NonNull Context context,
                         @NonNull AttributeSet attrs, boolean inheritContext,
                         boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
       final Context originalContext = context;

       // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
       // by using the parent's context
       if (inheritContext && parent != null) {
           context = parent.getContext();
       }
       if (readAndroidTheme || readAppTheme) {
           // We then apply the theme on the context, if specified
           context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
       }

       View view = null;

       // We need to 'inject' our tint aware Views in place of the standard framework versions
       switch (name) {
           case "TextView":
               view = createTextView(context, attrs);
               verifyNotNull(view, name);
               break;
           case "ImageView":
               view = createImageView(context, attrs);
               verifyNotNull(view, name);
               break;
           case "Button":
               view = createButton(context, attrs);
               verifyNotNull(view, name);
               break;
           case "EditText":
               view = createEditText(context, attrs);
               verifyNotNull(view, name);
               break;
           case "Spinner":
               view = createSpinner(context, attrs);
               verifyNotNull(view, name);
               break;
           case "ImageButton":
               view = createImageButton(context, attrs);
               verifyNotNull(view, name);
               break;
           case "CheckBox":
               view = createCheckBox(context, attrs);
               verifyNotNull(view, name);
               break;
           case "RadioButton":
               view = createRadioButton(context, attrs);
               verifyNotNull(view, name);
               break;
           case "CheckedTextView":
               view = createCheckedTextView(context, attrs);
               verifyNotNull(view, name);
               break;
           case "AutoCompleteTextView":
               view = createAutoCompleteTextView(context, attrs);
               verifyNotNull(view, name);
               break;
           case "MultiAutoCompleteTextView":
               view = createMultiAutoCompleteTextView(context, attrs);
               verifyNotNull(view, name);
               break;
           case "RatingBar":
               view = createRatingBar(context, attrs);
               verifyNotNull(view, name);
               break;
           case "SeekBar":
               view = createSeekBar(context, attrs);
               verifyNotNull(view, name);
               break;
           case "ToggleButton":
               view = createToggleButton(context, attrs);
               verifyNotNull(view, name);
               break;
           default:
               // The fallback that allows extending class to take over view inflation
               // for other tags. Note that we don't check that the result is not-null.
               // That allows the custom inflater path to fall back on the default one
               // later in this method.
               view = createView(context, name, attrs);
       }

       if (view == null) {
           // If the original context does not equal our themed context, then we need to manually
           // inflate it using the name so that android:theme takes effect.
           view = createViewFromTag(context, name, attrs);
       }

       if (view != null) {
           // If we have created a view, check its android:onClick
           checkOnClickListener(view, attrs);
       }

       return view;
   }

   @NonNull
   protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
       return new AppCompatTextView(context, attrs);
   }

   @NonNull
   protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
       return new AppCompatImageView(context, attrs);
   }

   @NonNull
   protected AppCompatButton createButton(Context context, AttributeSet attrs) {
       return new AppCompatButton(context, attrs);
   }

   @NonNull
   protected AppCompatEditText createEditText(Context context, AttributeSet attrs) {
       return new AppCompatEditText(context, attrs);
   }

   @NonNull
   protected AppCompatSpinner createSpinner(Context context, AttributeSet attrs) {
       return new AppCompatSpinner(context, attrs);
   }

   @NonNull
   protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) {
       return new AppCompatImageButton(context, attrs);
   }

   @NonNull
   protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs) {
       return new AppCompatCheckBox(context, attrs);
   }

   @NonNull
   protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) {
       return new AppCompatRadioButton(context, attrs);
   }

   @NonNull
   protected AppCompatCheckedTextView createCheckedTextView(Context context, AttributeSet attrs) {
       return new AppCompatCheckedTextView(context, attrs);
   }

   @NonNull
   protected AppCompatAutoCompleteTextView createAutoCompleteTextView(Context context,
                                                                      AttributeSet attrs) {
       return new AppCompatAutoCompleteTextView(context, attrs);
   }

   @NonNull
   protected AppCompatMultiAutoCompleteTextView createMultiAutoCompleteTextView(Context context,
                                                                                AttributeSet attrs) {
       return new AppCompatMultiAutoCompleteTextView(context, attrs);
   }

   @NonNull
   protected AppCompatRatingBar createRatingBar(Context context, AttributeSet attrs) {
       return new AppCompatRatingBar(context, attrs);
   }

   @NonNull
   protected AppCompatSeekBar createSeekBar(Context context, AttributeSet attrs) {
       return new AppCompatSeekBar(context, attrs);
   }

   @NonNull
   protected AppCompatToggleButton createToggleButton(Context context, AttributeSet attrs) {
       return new AppCompatToggleButton(context, attrs);
   }

   private void verifyNotNull(View view, String name) {
       if (view == null) {
           throw new IllegalStateException(this.getClass().getName()
                   + " asked to inflate view for <" + name + ">, but returned null");
       }
   }

   @Nullable
   protected View createView(Context context, String name, AttributeSet attrs) {
       return null;
   }

   private View createViewFromTag(Context context, String name, AttributeSet attrs) {
       if (name.equals("view")) {
           name = attrs.getAttributeValue(null, "class");
       }

       try {
           mConstructorArgs[0] = context;
           mConstructorArgs[1] = attrs;

           if (-1 == name.indexOf('.')) {
               for (int i = 0; i < sClassPrefixList.length; i++) {
                   final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
                   if (view != null) {
                       return view;
                   }
               }
               return null;
           } else {
               return createViewByPrefix(context, name, null);
           }
       } catch (Exception e) {
           // We do not want to catch these, lets return null and let the actual LayoutInflater
           // try
           return null;
       } finally {
           // Don't retain references on context.
           mConstructorArgs[0] = null;
           mConstructorArgs[1] = null;
       }
   }

   /**
    * android:onClick doesn't handle views with a ContextWrapper context. This method
    * backports new framework functionality to traverse the Context wrappers to find a
    * suitable target.
    */
   private void checkOnClickListener(View view, AttributeSet attrs) {
       final Context context = view.getContext();

       if (!(context instanceof ContextWrapper) ||
               (Build.VERSION.SDK_INT >= 15 && !ViewCompat.hasOnClickListeners(view))) {
           // Skip our compat functionality if: the Context isn't a ContextWrapper, or
           // the view doesn't have an OnClickListener (we can only rely on this on API 15+ so
           // always use our compat code on older devices)
           return;
       }

       final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs);
       final String handlerName = a.getString(0);
       if (handlerName != null) {
           view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
       }
       a.recycle();
   }

   private View createViewByPrefix(Context context, String name, String prefix)
           throws ClassNotFoundException, InflateException {
       Constructor<? extends View> constructor = sConstructorMap.get(name);

       try {
           if (constructor == null) {
               // Class not found in the cache, see if it's real, and try to add it
               Class<? extends View> clazz = Class.forName(
                       prefix != null ? (prefix + name) : name,
                       false,
                       context.getClassLoader()).asSubclass(View.class);

               constructor = clazz.getConstructor(sConstructorSignature);
               sConstructorMap.put(name, constructor);
           }
           constructor.setAccessible(true);
           return constructor.newInstance(mConstructorArgs);
       } catch (Exception e) {
           // We do not want to catch these, lets return null and let the actual LayoutInflater
           // try
           return null;
       }
   }

   /**
    * Allows us to emulate the {@code android:theme} attribute for devices before L.
    */
   private static Context themifyContext(Context context, AttributeSet attrs,
                                         boolean useAndroidTheme, boolean useAppTheme) {
       final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0);
       int themeId = 0;
       if (useAndroidTheme) {
           // First try reading android:theme if enabled
           themeId = a.getResourceId(R.styleable.View_android_theme, 0);
       }
       if (useAppTheme && themeId == 0) {
           // ...if that didn't work, try reading app:theme (for legacy reasons) if enabled
           themeId = a.getResourceId(R.styleable.View_theme, 0);

           if (themeId != 0) {
               Log.i(LOG_TAG, "app:theme is now deprecated. "
                       + "Please move to using android:theme instead.");
           }
       }
       a.recycle();

       if (themeId != 0 && (!(context instanceof ContextThemeWrapper)
               || ((ContextThemeWrapper) context).getThemeResId() != themeId)) {
           // If the context isn't a ContextThemeWrapper, or it is but does not have
           // the same theme as we need, wrap it in a new wrapper
           context = new ContextThemeWrapper(context, themeId);
       }
       return context;
   }

   /**
    * An implementation of OnClickListener that attempts to lazily load a
    * named click handling method from a parent or ancestor context.
    */
   private static class DeclaredOnClickListener implements View.OnClickListener {
       private final View mHostView;
       private final String mMethodName;

       private Method mResolvedMethod;
       private Context mResolvedContext;

       public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
           mHostView = hostView;
           mMethodName = methodName;
       }

       @Override
       public void onClick(@NonNull View v) {
           if (mResolvedMethod == null) {
               resolveMethod(mHostView.getContext());
           }

           try {
               mResolvedMethod.invoke(mResolvedContext, v);
           } catch (IllegalAccessException e) {
               throw new IllegalStateException(
                       "Could not execute non-public method for android:onClick", e);
           } catch (InvocationTargetException e) {
               throw new IllegalStateException(
                       "Could not execute method for android:onClick", e);
           }
       }

       private void resolveMethod(@Nullable Context context) {
           while (context != null) {
               try {
                   if (!context.isRestricted()) {
                       final Method method = context.getClass().getMethod(mMethodName, View.class);
                       if (method != null) {
                           mResolvedMethod = method;
                           mResolvedContext = context;
                           return;
                       }
                   }
               } catch (NoSuchMethodException e) {
                   // Failed to find method, keep searching up the hierarchy.
               }

               if (context instanceof ContextWrapper) {
                   context = ((ContextWrapper) context).getBaseContext();
               } else {
                   // Can't search up the hierarchy, null out and fail.
                   context = null;
               }
           }

           final int id = mHostView.getId();
           final String idText = id == View.NO_ID ? "" : " with id '"
                   + mHostView.getContext().getResources().getResourceEntryName(id) + "'";
           throw new IllegalStateException("Could not find method " + mMethodName
                   + "(View) in a parent or ancestor Context for android:onClick "
                   + "attribute defined on view " + mHostView.getClass() + idText);
       }
   }
}

  • 自定义Viewpager,监听滚动以实现对布局中的view进行平移
package com.crystal.view.parallax

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.crystal.view.R

/**
* 自定义ViewPager,监听滚动以用于View平移
* on 2022/11/14
*/
class ParallaxViewPager : ViewPager {
   private val fragments = arrayListOf<ParallaxFragment>()

   constructor(context: Context) : this(context, null)
   constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

   /**
    * 上一次滚动时的positionOffset 用于判断当前是左滑还是右滑
    */
   private var lastPositionOffset = 0f
   fun setLayout(fm: FragmentManager, layoutIds: IntArray) {
       fragments.clear()
       for (layoutId in layoutIds) {
           val fragment = ParallaxFragment()
           val bundle = Bundle()
           bundle.putInt(ParallaxFragment.LAYOUT_ID_KEY, layoutId)
           fragment.arguments = bundle
           fragments.add(fragment)
       }
       //设置adapter
       adapter = ParallaxPagerAdapter(fm)
       addOnPageChangeListener(object : OnPageChangeListener {
           override fun onPageScrolled(
               position: Int, positionOffset: Float, positionOffsetPixels: Int
           ) {
               Log.e("positionOffsetPixels","$positionOffsetPixels")
               /** positionOffsetPixels 屏幕宽度 右滑变化 0 - 屏幕宽度  左滑变化 屏幕宽度 - 0
                * 向左滑动 内容向右平移 当前内容出去
                * 向右滑动 内容向左平移 当前内容进入
                */
               val outParallaxViews = fragments[position].getParallaxViews()
               for (parallaxView in outParallaxViews) {
                   val outParallaxViewTag = parallaxView.getTag(R.id.parallax_tag) as ParallaxTag
                   parallaxView.translationX =
                       -outParallaxViewTag.translationXOut * positionOffsetPixels
                   parallaxView.translationY =
                       -outParallaxViewTag.translationYOut * positionOffsetPixels
               }

               try {
                   val inParallaxViews = fragments[position + 1].getParallaxViews()
                   for (parallaxView in inParallaxViews) {
                       val inParallaxViewTag = parallaxView.tag as ParallaxTag
                       parallaxView.translationX =
                           inParallaxViewTag.translationXIn * (measuredWidth - positionOffsetPixels)
                       parallaxView.translationY =
                           inParallaxViewTag.translationYIn * (measuredWidth - positionOffsetPixels)
                   }
               } catch (e: Exception) {
               }
           }

           override fun onPageSelected(position: Int) {

           }

           override fun onPageScrollStateChanged(state: Int) {

           }

       })
   }


   private inner class ParallaxPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
       override fun getCount(): Int {
           return fragments.size
       }

       override fun getItem(position: Int): Fragment {
           return fragments[position]
       }

   }

}

测试验证

   val viewpager = findViewById<ParallaxViewPager>(R.id.viewpager)
        viewpager.setLayout(
            supportFragmentManager,
            intArrayOf(
                R.layout.fragment_page,
                R.layout.fragment_page,
                R.layout.fragment_page
            )
        )

总结

通过解析自定义属性,学习了LayoutInflater是如何完成View的加载工作,对以后的工作很有帮助。

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值