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

上一篇已经讲了让系统控件如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表示当前页面已滑动了一半。

扫描二维码关注公众号,回复: 8682208 查看本文章

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

发布了44 篇原创文章 · 获赞 27 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/gaoxiaoweiandy/article/details/89493652
今日推荐