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

news/2024/7/2 9:00:44

前言

在学习本文之前,可以先了解下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的加载工作,对以后的工作很有帮助。

结语

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


http://lihuaxi.xjx100.cn/news/158203.html

相关文章

java家庭理财收支管理系统

目 录 1 绪论 1 1.1 引言 1 1.2 研究内容 2 1.3 可行性分析 2 1.3.1 技术可行性 2 1.3.2 经济可行性 2 1.3.3 操作可行性 3 1.4 研究现状 3 2 开发技术介绍 4 2.1 Jsp技术 4 2.2 Mysql 5 2.3 tomcat简介 5 2.4 JDBC 6 3 需求分析 …

员工如何通过自助方式重置AD密码

很多企业都会通过AD管理员工的账户&#xff0c;而密码重置是IT helpdesk很繁重的一项工作。在OneAuth中可以启用员工自助修改密码。比如通过给员工手机/邮箱发送修改密码的临时口令&#xff0c;员工点输入临时口令验证&#xff0c;验证通过后才可以修改密码。 通过OneAuth重置…

Docker(一):什么是Docker?

为什么会出现Docker 假设你在开发一个项目&#xff0c;你所用的电脑是具备了项目特定配置的开发环境。但其他开发人员的设备以及开发环境配置都各有不同。你所在开发的应用需要依赖你当前的配置&#xff0c;而当你需要发布到测试环境的时候&#xff0c;你需要把你本地的环境配…

实现分布式下的全局唯一ID

ID生成规则必要性 软件上要求 全局唯一 不能出现重复的ID号&#xff0c;既然是唯一标识&#xff0c;这是最基本的要求趋势递增 在MySQL的InnoDB引擎中使用的是聚集索引&#xff0c;由于多数RDBMS使用Btree的数据结构来存储索引数据&#xff0c; 在主键的选择上面我们应该尽量…

RabbitMQ的入门篇

目录 1. RabbitMQ简介 1.1 RabbitMQ 1.2 RabbitMQ的介绍 1.3 RabbitMQ的特点 2. RabbitMQ的安装 2.1 RabbitMQ下载 2.2 下载的安装包 ​ 2.3 安装步骤 2.4 登录访问 3. RabbitMQ的入门 3.1 RabbitMQ的架构原理 3.2 永远的hello world程序 4. RabbitMQ的工作模式 4.1 简…

cnpm的版本锁定问题的解决方案

之前因为项目需求&#xff0c;经常使用cnpm i来下载依赖&#xff0c;后来有一次在debug的过程中发现用cnpm下载安装依赖是不会锁版本的&#xff0c;特此就这个问题在这里做个详细记录。 首先了解下npm包管理及依赖版本管理的原理。这些都是通过package.json文件实现的 当你使用…

spring框架中的IOC实现

spring IOC控制反转 这里先说一下IOC&#xff0c;再说IOC在spring框架中的使用。 IOC的概念 IOC这个缩写有很多意思&#xff0c;比如 智慧城市智能运行中心(IOC)奥林匹克运动的领导机构 但是呢&#xff0c;我们这里说的是面向对象编程中的一种设计原则。他的全称是Invers…

Android入门第25天-Android里使用SimpleAdapter实现复杂的界面布局

介绍 上一篇里我们讲到了以下这样的一个布局 它用的就是SimpleAdapter来实现的。 SimpleAdapter原理 我们来看一下SimpleAdapter的实现 //定义Listview内的元素内容private String[] name new String[]{"雷神", "基神", "天神"};private Str…