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

举报
yd_57386892 发表于 2020/12/28 23:28:31 2020/12/28
3.8k+ 0 0
【摘要】 上一篇已经讲了让系统控件如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滑动,然后每一个页面中的元素运动速度不一样,有的元素移动的速度明显比其它元素快,这样就形成了视差效果。首先我们来看一下这个框架的用法:


      package com.example.animateframe2;
      import android.os.Bundle;
      import android.support.v4.app.FragmentActivity;
      import android.widget.ImageView;
      public class SplashActivity extends FragmentActivity {
     	@Override
     	protected void onCreate(Bundle savedInstanceState) {
     		super.onCreate(savedInstanceState);
      		setContentView(R.layout.main);
      		ParallaxContainer container = (ParallaxContainer) findViewById(R.id.parallax_container);
      		container.setUp(new int[]{
      			R.layout.view_intro_1,
      			R.layout.view_intro_2,
      			R.layout.view_intro_3,
      			R.layout.view_intro_4,
      			R.layout.view_intro_5,
      			R.layout.view_login
      		});
     		//设置动画
      		ImageView iv_man = (ImageView) findViewById(R.id.iv_man);
      		iv_man.setBackgroundResource(R.drawable.man_run);
      		container.setIv_man(iv_man);
      	}
      }
  
 

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

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


      package com.example.animateframe2;
      import java.util.ArrayList;
      import java.util.List;
      import com.nineoldandroids.view.ViewHelper;
      import android.content.Context;
      import android.graphics.drawable.AnimationDrawable;
      import android.os.Bundle;
      import android.support.v4.view.ViewPager;
      import android.support.v4.view.ViewPager.OnPageChangeListener;
      import android.util.AttributeSet;
      import android.util.Log;
      import android.view.View;
      import android.widget.FrameLayout;
      import android.widget.ImageView;
      /**
       * 引导页的最外层布局
       */
      public class ParallaxContainer extends FrameLayout implements OnPageChangeListener {
     	private List<ParallaxFragment> fragments;
     	private ParallaxPagerAdapter adapter;
     	private float containerWidth;
     	private ImageView iv_man;
     	public ParallaxContainer(Context context, AttributeSet attrs) {
     		super(context, attrs);
      	}
     	/**
       * 指定引导页的所有页面布局文件
       * @param childIds
       */
     	public void setUp(int... childIds){
     		//根据布局文件数组,初始化所有的fragment
      		fragments = new ArrayList<ParallaxFragment>();
     		for (int i = 0; i < childIds.length; i++) {
      			ParallaxFragment f = new ParallaxFragment();
      			Bundle args = new Bundle();
     			//页面索引
      			args.putInt("index", i);
     			//Fragment中需要加载的布局文件id
      			args.putInt("layoutId", childIds[i]);
      			f.setArguments(args);
      			fragments.add(f);
      		}
     		//实例化适配器
      		SplashActivity activity = (SplashActivity)getContext();
      		adapter = new ParallaxPagerAdapter(activity.getSupportFragmentManager(), fragments);
     		//实例化ViewPager
      		ViewPager vp = new ViewPager(getContext());
      		vp.setId(R.id.parallax_pager);
      		vp.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
     		//绑定
      		vp.setAdapter(adapter);
      		addView(vp,0);
      		vp.setOnPageChangeListener(this);
      	}
     	@Override
     	public void onPageScrolled(int position, float positionOffset,
     			int positionOffsetPixels) {
     		this.containerWidth = getWidth();
     		//在翻页的过程中,不断根据视图的标签中对应的动画参数,改变视图的位置或者透明度
     		//获取到进入的页面
      		ParallaxFragment inFragment = null;
     		try {
      			inFragment = fragments.get(position - 1);
      		} catch (Exception e) {}
     		//获取到退出的页面
      		ParallaxFragment outFragment = null;
     		try {
      			outFragment = fragments.get(position);
      		} catch (Exception e) {}
     		if (inFragment != null) {
     			//获取Fragment上所有的视图,实现动画效果
      			List<View> inViews = inFragment.getParallaxViews();
     			if (inViews != null) {
      for (View view : inViews) {
      //获取标签,从标签上获取所有的动画参数
       ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
      if (tag == null) {
      continue;
       }
      //translationY改变view的偏移位置,translationY=100,代表view在其原始位置向下移动100
      //仔细观察进入的fragment中view从远处过来,不断向下移动,最终停在原始位置
       ViewHelper.setTranslationY(view, (containerWidth - positionOffsetPixels) * tag.yIn);
       ViewHelper.setTranslationX(view, (containerWidth - positionOffsetPixels) * tag.xIn);
       }
      			}
      		}
     		if(outFragment != null){
      			List<View> outViews = outFragment.getParallaxViews();
     			if (outViews != null) {
      for (View view : outViews) {
       ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
      if (tag == null) {
      continue;
       }
      //仔细观察退出的fragment中view从原始位置开始向上移动,translationY应为负数
       ViewHelper.setTranslationY(view, 0 - positionOffsetPixels * tag.yOut);
       ViewHelper.setTranslationX(view, 0 - positionOffsetPixels * tag.xOut);
       }
      			}
      		}
      	}
     	@Override
     	public void onPageSelected(int position) {
     		if (position == adapter.getCount() - 1) {
      			iv_man.setVisibility(INVISIBLE);
      		}else{
      			iv_man.setVisibility(VISIBLE);
      		}
      	}
     	@Override
     	public void onPageScrollStateChanged(int state) {
      		AnimationDrawable animation = (AnimationDrawable) iv_man.getBackground();
     		switch (state) {
     		case ViewPager.SCROLL_STATE_DRAGGING:
      			animation.start();
     			break;
     		case ViewPager.SCROLL_STATE_IDLE:
      			animation.stop();
     			break;
     		default:
     			break;
      		}
      	}
     	public void setIv_man(ImageView iv_man) {
     		this.iv_man = iv_man;
      	}
      }
  
 

正如我们所分析的,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代码:


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

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

        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:


      package com.example.animateframe2;
      import android.content.Context;
      import android.content.res.TypedArray;
      import android.util.AttributeSet;
      import android.util.Log;
      import android.view.LayoutInflater;
      import android.view.View;
      public class ParallaxLayoutInflater extends LayoutInflater {
     	private ParallaxFragment fragment;
     	protected ParallaxLayoutInflater(LayoutInflater original, Context newContext,ParallaxFragment fragment) {
     		super(original, newContext);
     		this.fragment = fragment;
     		//重新设置布局加载器的工厂
     		//工厂:创建布局文件中所有的视图
      		setFactory2(new ParallaxFactory(this));
      	}
     	@Override
     	public LayoutInflater cloneInContext(Context newContext) {
     		return this;
     		//return new ParallaxLayoutInflater(this,newContext,fragment);
      	}
     	class ParallaxFactory implements Factory2{
     		private LayoutInflater inflater;
     		private final String[] sClassPrefix = {
      "android.widget.",
      "android.view."
      		};
     		public ParallaxFactory(LayoutInflater inflater) {
     			this.inflater = inflater;
      		}
     		//自定义,视图创建的过程
     		@Override
     		public View onCreateView(String name, Context context,
       AttributeSet attrs) {
      			View view = null;
     			if (view == null) {
       view = createViewOrFailQuietly(name,context,attrs);
      			}
     			//实例化完成
     			if (view != null) {
      //获取自定义属性,通过标签关联到视图上
       setViewTag(view,context,attrs);
       fragment.getParallaxViews().add(view);
       Log.d("ricky", "view:"+view);
      			}
     			return view;
      		}
     		private void setViewTag(View view, Context context, AttributeSet attrs) {
     			//所有自定义的属性
     			int[] attrIds = {
       R.attr.a_in,
       R.attr.a_out,
       R.attr.x_in,
       R.attr.x_out,
       R.attr.y_in,
       R.attr.y_out};
     			//获取
      			TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
     			if (a != null && a.length() > 0) {
      //获取自定义属性的值
       ParallaxViewTag tag = new ParallaxViewTag();
       tag.alphaIn = a.getFloat(0, 0f);
       tag.alphaOut = a.getFloat(1, 0f);
       tag.xIn = a.getFloat(2, 0f);
       tag.xOut = a.getFloat(3, 0f);
       tag.yIn = a.getFloat(4, 0f);
       tag.yOut = a.getFloat(5, 0f);
      //index
       view.setTag(R.id.parallax_view_tag,tag);
      			}
      			a.recycle();
      		}
     		private View createViewOrFailQuietly(String name, String prefix,Context context,
       AttributeSet attrs) {
     			try {
      //通过系统的inflater创建视图,读取系统的属性
      return inflater.createView(name, prefix, attrs);
      			} catch (Exception e) {
      return null;
      			}
      		}
     		private View createViewOrFailQuietly(String name, Context context,
       AttributeSet attrs) {
     			//1.自定义控件标签名称带点,所以创建时不需要前缀
     			if (name.contains(".")) {
       createViewOrFailQuietly(name, null, context, attrs);
      			}
     			//2.系统视图需要加上前缀
     			for (String prefix : sClassPrefix) {
       View view = createViewOrFailQuietly(name, prefix, context, attrs);
      if (view != null) {
      return view;
       }
      			}
     			return null;
      		}
     		@Override
     		public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
      			View view = null;
     			if (view == null) {
       view = createViewOrFailQuietly(name,context,attrs);
      			}
     			//实例化完成
     			if (view != null) {
      //获取自定义属性,通过标签关联到视图上
       setViewTag(view,context,attrs);
       fragment.getParallaxViews().add(view);
       Log.d("ricky", "view:"+view);
      			}
     			return view;
      		}
      	}
      }
  
 

代码分析:

1. 首先继承LayoutInflater

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

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

 

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

      "android.widget.",
      "android.view."
  
 

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

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

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

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


     	private void setViewTag(View view, Context context, AttributeSet attrs) {
     			//所有自定义的属性
     			int[] attrIds = {
       R.attr.a_in,
       R.attr.a_out,
       R.attr.x_in,
       R.attr.x_out,
       R.attr.y_in,
       R.attr.y_out};
     			//获取
      			TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
     			if (a != null && a.length() > 0) {
      //获取自定义属性的值
       ParallaxViewTag tag = new ParallaxViewTag();
       tag.alphaIn = a.getFloat(0, 0f);
       tag.alphaOut = a.getFloat(1, 0f);
       tag.xIn = a.getFloat(2, 0f);
       tag.xOut = a.getFloat(3, 0f);
       tag.yIn = a.getFloat(4, 0f);
       tag.yOut = a.getFloat(5, 0f);
      //index
       view.setTag(R.id.parallax_view_tag,tag);
      			}
      			a.recycle();
      		}
  
 

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

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

    全部回复

    上滑加载中

    设置昵称

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

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

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