Android高级UI开发(三十)让系统控件识别自定义属性的第二种方法:视差动画框架实例

举报
yd_57386892 发表于 2020/12/28 23:28:31 2020/12/28
【摘要】 上一篇已经讲了让系统控件如Imageview识别自定义属性的方法一: 自定义LinearLayout,在addview的时候,给每一个系统控件外层再包裹一个自定义VIEWGROUP,然后这个VIEWGROUP来识别自定义属性并执行动画。今天我们讲第二种方法,与第一种方法有点雷同,就是在解析XML中的系统控件时,解析出属性及属性值,把这个控件如Imageview上的自定义属性解...

上一篇已经讲了让系统控件如Imageview识别自定义属性的方法一: 自定义LinearLayout,在addview的时候,给每一个系统控件外层再包裹一个自定义VIEWGROUP,然后这个VIEWGROUP来识别自定义属性并执行动画。今天我们讲第二种方法,与第一种方法有点雷同,就是在解析XML中的系统控件时,解析出属性及属性值,把这个控件如Imageview上的自定义属性解析出来封装在一个TAG对象里,然后使用view.setTag方法将自定义属性与这个系统控件关联起来。当我们执行动画时,可以使用view.getTag获取这个系统控件身上的自定义属性,执行相应的动画如水平移动,垂直移动等。我们先看一下今天要展示的实例运行效果:

源码下载地址:https://download.csdn.net/download/gaoxiaoweiandy/11142066

 

这个效果是一个ViewPager滑动,然后每一个页面中的元素运动速度不一样,有的元素移动的速度明显比其它元素快,这样就形成了视差效果。首先我们来看一下这个框架的用法:


  
  1. package com.example.animateframe2;
  2. import android.os.Bundle;
  3. import android.support.v4.app.FragmentActivity;
  4. import android.widget.ImageView;
  5. public class SplashActivity extends FragmentActivity {
  6. @Override
  7. protected void onCreate(Bundle savedInstanceState) {
  8. super.onCreate(savedInstanceState);
  9. setContentView(R.layout.main);
  10. ParallaxContainer container = (ParallaxContainer) findViewById(R.id.parallax_container);
  11. container.setUp(new int[]{
  12. R.layout.view_intro_1,
  13. R.layout.view_intro_2,
  14. R.layout.view_intro_3,
  15. R.layout.view_intro_4,
  16. R.layout.view_intro_5,
  17. R.layout.view_login
  18. });
  19. //设置动画
  20. ImageView iv_man = (ImageView) findViewById(R.id.iv_man);
  21. iv_man.setBackgroundResource(R.drawable.man_run);
  22. container.setIv_man(iv_man);
  23. }
  24. }

 

为ParallaxContainer设置了几个布局,将来这几个布局可以滑动,然后还有自定义属性动画。就这么简单。下面那个iv_man就是一个简单的播放帧动画(小人走路)。

我们从最初的框架使用方法倒推这个动画框架的内部实现:这个ParallaxContainer,既然可以滑动,里面应该包含了ViewPager, 滑动时执行动画,说明我们为ViewPager设置了页面滑动监听“OnPageChangeListener”,在这个监听器里根据滑动的距离来执行动画。 滑动的每一个页面是一个Fragment。 我们来看看ParallaxContainer的代码是不是包含这些,代码如下:


  
  1. package com.example.animateframe2;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import com.nineoldandroids.view.ViewHelper;
  5. import android.content.Context;
  6. import android.graphics.drawable.AnimationDrawable;
  7. import android.os.Bundle;
  8. import android.support.v4.view.ViewPager;
  9. import android.support.v4.view.ViewPager.OnPageChangeListener;
  10. import android.util.AttributeSet;
  11. import android.util.Log;
  12. import android.view.View;
  13. import android.widget.FrameLayout;
  14. import android.widget.ImageView;
  15. /**
  16. * 引导页的最外层布局
  17. */
  18. public class ParallaxContainer extends FrameLayout implements OnPageChangeListener {
  19. private List<ParallaxFragment> fragments;
  20. private ParallaxPagerAdapter adapter;
  21. private float containerWidth;
  22. private ImageView iv_man;
  23. public ParallaxContainer(Context context, AttributeSet attrs) {
  24. super(context, attrs);
  25. }
  26. /**
  27. * 指定引导页的所有页面布局文件
  28. * @param childIds
  29. */
  30. public void setUp(int... childIds){
  31. //根据布局文件数组,初始化所有的fragment
  32. fragments = new ArrayList<ParallaxFragment>();
  33. for (int i = 0; i < childIds.length; i++) {
  34. ParallaxFragment f = new ParallaxFragment();
  35. Bundle args = new Bundle();
  36. //页面索引
  37. args.putInt("index", i);
  38. //Fragment中需要加载的布局文件id
  39. args.putInt("layoutId", childIds[i]);
  40. f.setArguments(args);
  41. fragments.add(f);
  42. }
  43. //实例化适配器
  44. SplashActivity activity = (SplashActivity)getContext();
  45. adapter = new ParallaxPagerAdapter(activity.getSupportFragmentManager(), fragments);
  46. //实例化ViewPager
  47. ViewPager vp = new ViewPager(getContext());
  48. vp.setId(R.id.parallax_pager);
  49. vp.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
  50. //绑定
  51. vp.setAdapter(adapter);
  52. addView(vp,0);
  53. vp.setOnPageChangeListener(this);
  54. }
  55. @Override
  56. public void onPageScrolled(int position, float positionOffset,
  57. int positionOffsetPixels) {
  58. this.containerWidth = getWidth();
  59. //在翻页的过程中,不断根据视图的标签中对应的动画参数,改变视图的位置或者透明度
  60. //获取到进入的页面
  61. ParallaxFragment inFragment = null;
  62. try {
  63. inFragment = fragments.get(position - 1);
  64. } catch (Exception e) {}
  65. //获取到退出的页面
  66. ParallaxFragment outFragment = null;
  67. try {
  68. outFragment = fragments.get(position);
  69. } catch (Exception e) {}
  70. if (inFragment != null) {
  71. //获取Fragment上所有的视图,实现动画效果
  72. List<View> inViews = inFragment.getParallaxViews();
  73. if (inViews != null) {
  74. for (View view : inViews) {
  75. //获取标签,从标签上获取所有的动画参数
  76. ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
  77. if (tag == null) {
  78. continue;
  79. }
  80. //translationY改变view的偏移位置,translationY=100,代表view在其原始位置向下移动100
  81. //仔细观察进入的fragment中view从远处过来,不断向下移动,最终停在原始位置
  82. ViewHelper.setTranslationY(view, (containerWidth - positionOffsetPixels) * tag.yIn);
  83. ViewHelper.setTranslationX(view, (containerWidth - positionOffsetPixels) * tag.xIn);
  84. }
  85. }
  86. }
  87. if(outFragment != null){
  88. List<View> outViews = outFragment.getParallaxViews();
  89. if (outViews != null) {
  90. for (View view : outViews) {
  91. ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
  92. if (tag == null) {
  93. continue;
  94. }
  95. //仔细观察退出的fragment中view从原始位置开始向上移动,translationY应为负数
  96. ViewHelper.setTranslationY(view, 0 - positionOffsetPixels * tag.yOut);
  97. ViewHelper.setTranslationX(view, 0 - positionOffsetPixels * tag.xOut);
  98. }
  99. }
  100. }
  101. }
  102. @Override
  103. public void onPageSelected(int position) {
  104. if (position == adapter.getCount() - 1) {
  105. iv_man.setVisibility(INVISIBLE);
  106. }else{
  107. iv_man.setVisibility(VISIBLE);
  108. }
  109. }
  110. @Override
  111. public void onPageScrollStateChanged(int state) {
  112. AnimationDrawable animation = (AnimationDrawable) iv_man.getBackground();
  113. switch (state) {
  114. case ViewPager.SCROLL_STATE_DRAGGING:
  115. animation.start();
  116. break;
  117. case ViewPager.SCROLL_STATE_IDLE:
  118. animation.stop();
  119. break;
  120. default:
  121. break;
  122. }
  123. }
  124. public void setIv_man(ImageView iv_man) {
  125. this.iv_man = iv_man;
  126. }
  127. }

正如我们所分析的,setUp函数里确实为ViewPager设置了一个以frament数组为页面列表的adapter.还有一个

onPageScrolled函数来监听滑动的距离,根据页面滑出,进入的比例执行动画。我们来看一下onPageScrolled的各个参数:
 
onPageScrolled(int position, float positionOffset,int positionOffsetPixels) 
 

position:当前正在滑动的页面,

positionOffset: 滑动的比例,例如0.5表示当前页面已滑动了一半。

positionOffsetPixels: 滑动的比例对应的屏幕像素值,如屏幕宽度的一半像素。

 

然后这个onPageScrolled里面就是执行动画的代码,首先使用view.getTag来获取与view元素关联的自定义属性对象(里面包含了这个view设置的所有自定义属性及值),然后调用setTranslationY这类的平移函数来实现VIEW元素平移动画。其中inFragment是进入的页面,outFragment是要滑出的页面。 这里面的滑动算法我们无需关心,随自己定。我们主要关心的是这个view.getTag获取系统控件(如Imageview)的自定义属性,这个TAG是什么时候set进去的,以及这个tag里的自定义属性值是怎么获取到的。有于每一个页面中的布局是在Fragment里加载的,因此我们来看一下Fragment加载布局的代码。

ParallaxFragment.java代码:


  
  1. package com.example.animateframe2;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import android.os.Bundle;
  5. import android.support.v4.app.Fragment;
  6. import android.util.Log;
  7. import android.view.LayoutInflater;
  8. import android.view.View;
  9. import android.view.ViewGroup;
  10. public class ParallaxFragment extends Fragment {
  11. //此Fragment上所有的需要实现视差动画的视图
  12. private List<View> parallaxViews = new ArrayList<View>();
  13. @Override
  14. public View onCreateView(LayoutInflater original, ViewGroup container,
  15. Bundle savedInstanceState) {
  16. Bundle args = getArguments();
  17. int layoutId = args.getInt("layoutId");
  18. int index = args.getInt("index");
  19. Log.d("jason", "fragment:"+index);
  20. //1.布局加载器将布局加载进来了
  21. //2.解析创建布局上所有的视图
  22. //3.自己搞定创建视图的过程
  23. //4.获取视图相关的自定义属性的值
  24. ParallaxLayoutInflater inflater = new ParallaxLayoutInflater(original, getActivity(),this);
  25. return inflater.inflate(layoutId, null);
  26. }
  27. public List<View> getParallaxViews() {
  28. return parallaxViews;
  29. }
  30. }

 

我们看一下关键的两行代码:

        ParallaxLayoutInflater inflater = new ParallaxLayoutInflater(original, getActivity(),this);
        
        return inflater.inflate(layoutId, null);

我们自定义了一个LayoutInflater:  ParallaxLayoutInflater,用自定义的ParallaxLayoutInflater去解析XML布局layoutId.

我们自定义LayoutInflater的目的,无非是想在解析XML里的各个VIEW时做些手脚,做什么手脚呢? 解析每一个XML的view元素,并获取它身上的多个自定义属性,封装在一个tag对象里,然后调用view.setTag将view与自定义属性关联起来。

通过阅读系统LayoutInflater源码,系统API是在LayoutInflater的Factory2对象里的onCreateView函数里来解析xml元素的。因此我们想办法自定义LayoutInflater,并重写Factory2的onCreateView函数。我们先来看一下ParallaxLayoutInflater的代码,

自定义LayoutInflater:ParallaxLayoutInflater.java:


  
  1. package com.example.animateframe2;
  2. import android.content.Context;
  3. import android.content.res.TypedArray;
  4. import android.util.AttributeSet;
  5. import android.util.Log;
  6. import android.view.LayoutInflater;
  7. import android.view.View;
  8. public class ParallaxLayoutInflater extends LayoutInflater {
  9. private ParallaxFragment fragment;
  10. protected ParallaxLayoutInflater(LayoutInflater original, Context newContext,ParallaxFragment fragment) {
  11. super(original, newContext);
  12. this.fragment = fragment;
  13. //重新设置布局加载器的工厂
  14. //工厂:创建布局文件中所有的视图
  15. setFactory2(new ParallaxFactory(this));
  16. }
  17. @Override
  18. public LayoutInflater cloneInContext(Context newContext) {
  19. return this;
  20. //return new ParallaxLayoutInflater(this,newContext,fragment);
  21. }
  22. class ParallaxFactory implements Factory2{
  23. private LayoutInflater inflater;
  24. private final String[] sClassPrefix = {
  25. "android.widget.",
  26. "android.view."
  27. };
  28. public ParallaxFactory(LayoutInflater inflater) {
  29. this.inflater = inflater;
  30. }
  31. //自定义,视图创建的过程
  32. @Override
  33. public View onCreateView(String name, Context context,
  34. AttributeSet attrs) {
  35. View view = null;
  36. if (view == null) {
  37. view = createViewOrFailQuietly(name,context,attrs);
  38. }
  39. //实例化完成
  40. if (view != null) {
  41. //获取自定义属性,通过标签关联到视图上
  42. setViewTag(view,context,attrs);
  43. fragment.getParallaxViews().add(view);
  44. Log.d("ricky", "view:"+view);
  45. }
  46. return view;
  47. }
  48. private void setViewTag(View view, Context context, AttributeSet attrs) {
  49. //所有自定义的属性
  50. int[] attrIds = {
  51. R.attr.a_in,
  52. R.attr.a_out,
  53. R.attr.x_in,
  54. R.attr.x_out,
  55. R.attr.y_in,
  56. R.attr.y_out};
  57. //获取
  58. TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
  59. if (a != null && a.length() > 0) {
  60. //获取自定义属性的值
  61. ParallaxViewTag tag = new ParallaxViewTag();
  62. tag.alphaIn = a.getFloat(0, 0f);
  63. tag.alphaOut = a.getFloat(1, 0f);
  64. tag.xIn = a.getFloat(2, 0f);
  65. tag.xOut = a.getFloat(3, 0f);
  66. tag.yIn = a.getFloat(4, 0f);
  67. tag.yOut = a.getFloat(5, 0f);
  68. //index
  69. view.setTag(R.id.parallax_view_tag,tag);
  70. }
  71. a.recycle();
  72. }
  73. private View createViewOrFailQuietly(String name, String prefix,Context context,
  74. AttributeSet attrs) {
  75. try {
  76. //通过系统的inflater创建视图,读取系统的属性
  77. return inflater.createView(name, prefix, attrs);
  78. } catch (Exception e) {
  79. return null;
  80. }
  81. }
  82. private View createViewOrFailQuietly(String name, Context context,
  83. AttributeSet attrs) {
  84. //1.自定义控件标签名称带点,所以创建时不需要前缀
  85. if (name.contains(".")) {
  86. createViewOrFailQuietly(name, null, context, attrs);
  87. }
  88. //2.系统视图需要加上前缀
  89. for (String prefix : sClassPrefix) {
  90. View view = createViewOrFailQuietly(name, prefix, context, attrs);
  91. if (view != null) {
  92. return view;
  93. }
  94. }
  95. return null;
  96. }
  97. @Override
  98. public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
  99. View view = null;
  100. if (view == null) {
  101. view = createViewOrFailQuietly(name,context,attrs);
  102. }
  103. //实例化完成
  104. if (view != null) {
  105. //获取自定义属性,通过标签关联到视图上
  106. setViewTag(view,context,attrs);
  107. fragment.getParallaxViews().add(view);
  108. Log.d("ricky", "view:"+view);
  109. }
  110. return view;
  111. }
  112. }
  113. }

 

代码分析:

1. 首先继承LayoutInflater

2. 然后自定义一个Factory2:  ParallaxFactory , 并重写onCreateview方法,调用

view = createViewOrFailQuietly(name,context,attrs);获取系统为我们解析的XML元素view实例。

 

  
  1. private View createViewOrFailQuietly(String name, Context context,
  2. AttributeSet attrs) {
  3. //1.自定义控件标签名称带点,所以创建时不需要前缀
  4. if (name.contains(".")) {
  5. createViewOrFailQuietly(name, null, context, attrs);
  6. }
  7. //2.系统视图需要加上前缀
  8. for (String prefix : sClassPrefix) {
  9. View view = createViewOrFailQuietly(name, prefix, context, attrs);
  10. if (view != null) {
  11. return view;
  12. }
  13. }
  14. return null;
  15. }
  16. private View createViewOrFailQuietly(String name, String prefix,Context context,
  17. AttributeSet attrs) {
  18. try {
  19. //通过系统的inflater创建视图,读取系统的属性
  20. return inflater.createView(name, prefix, attrs);
  21. } catch (Exception e) {
  22. return null;
  23. }
  24. }
为什么是系统帮我们解析的,因为在这个函数里面我们最终调用的是  return inflater.createView(name, prefix, attrs);
 
那为什么我们不直接调用系统的inflater.createView(name, prefix, attrs)函数来获取xml中的view实例呢。因为在这个createViewOrFailQuietly函数里我们要区分自定义控件与系统控件,如果是自定义控件的话,整个name就是一个完成的类路径,所以prefix为null.如何是系统控件,如imageview,那么prefix得传递系统控件前缀
 

  
  1. "android.widget.",
  2. "android.view."

OK,这一步我们获得了每一个XML中的view。

 

3.  获取自定义属性,封装到TAG

    
            if (view != null) {
                //获取自定义属性,通过标签关联到视图上
                setViewTag(view,context,attrs);
                fragment.getParallaxViews().add(view);
                Log.d("ricky", "view:"+view);
            }

这里setViewTag函数完成了这个功能:


  
  1. private void setViewTag(View view, Context context, AttributeSet attrs) {
  2. //所有自定义的属性
  3. int[] attrIds = {
  4. R.attr.a_in,
  5. R.attr.a_out,
  6. R.attr.x_in,
  7. R.attr.x_out,
  8. R.attr.y_in,
  9. R.attr.y_out};
  10. //获取
  11. TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
  12. if (a != null && a.length() > 0) {
  13. //获取自定义属性的值
  14. ParallaxViewTag tag = new ParallaxViewTag();
  15. tag.alphaIn = a.getFloat(0, 0f);
  16. tag.alphaOut = a.getFloat(1, 0f);
  17. tag.xIn = a.getFloat(2, 0f);
  18. tag.xOut = a.getFloat(3, 0f);
  19. tag.yIn = a.getFloat(4, 0f);
  20. tag.yOut = a.getFloat(5, 0f);
  21. //index
  22. view.setTag(R.id.parallax_view_tag,tag);
  23. }
  24. a.recycle();
  25. }

fragment.getParallaxViews().add(view);这行代码是将每一页fragment中的view元素放到一个数组里。以方便我们在前面的viewPager : onPageScrolled函数里遍历每一个view并获取这个view上的自定义属性。

Ok,至此我们从最初的框架使用方法倒推出了这个动画框架的内部实现。

源码下载地址:https://download.csdn.net/download/gaoxiaoweiandy/11142066

 

文章来源: blog.csdn.net,作者:冉航--小虾米,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/gaoxiaoweiandy/article/details/89493652

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200