使用一种独特的方式实现动画框架
在App中,为了展现的效果更佳,设计师通常都会加入一些动画的元素在里面,在现在的各类应用中,动画也变得越来越常见,今天,分享的是一种实现动画框架的思路,具体的效果图可以参考下面这个视差动画(只是例子):
从效果图看,要实现该效果,应该是不难的,正常操作都是给每一个View设置动画
如:view.setScaleX()……
但这样的话,虽然是效果实现了,但是复用性可以说为0,我们需要在每个用到的地方,都去设置他的动画属性。
下面,就来介绍一种很特别的实现方式:
我们可以在每个需要实现动画的View中加入自定义的参数,具体如下:
<ImageView
android:layout_width="200dp"
android:layout_height="120dp"
android:layout_gravity="top|right"
parallax:parallax_translation="fromLeft|fromBottom"
parallax:parallax_alpha="true"
android:src="@mipmap/baggage" />
在如上的代码中,我们除了看到平常使用的控件属性外,还有一些使我们自己定义的属性:
<!-- 平移 -->
parallax:parallax_translation="fromLeft|fromBottom"
<!-- 背景的透明度 -->
parallax:parallax_alpha="true"
这样一来,自定义的属性是加上了,但是,问题就来了,我们知道,只有系统控件才能识别系统的属性,自定义控件才能识别自定义的属性,这系统控件也不识别自定义属性啊?
别慌,方法还是有的,我们来看一个图:
我们可以在系统加载渲染控件的时候,偷偷地做件事情,给需要添加动画的空间外面包裹一层frameLayout,这样的话,当frameLayout执行动画的时候,其里面的子空间也就跟着一起执行动画了!
所以,我们需要自定义一个FrameLayout,让他来帮助我们完成动画。
那么问题又来了,我们在什么时机去给控件包裹这层FrameLayout呢?
答案当然是在控件的父控件去添加该View的时候,所以,我们可以在父控件的addView方法中来做处理,既然这样,我们就需要来自定义父容器,去干扰父容器添加子View的过程。在androi中,但弗雷去添加子view之前,会去解析子view的LayoutParams,该方法为generateLayoutParams(AttributeSet attrs)方法。具体的思路就是这样。
俗话说得好,自古~~~,唯有代码得人心。
自定义ViewGroup,这里以LinearLayout为例
public class ParallaxLinearLayout extends LinearLayout {
public ParallaxLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
ParallaxLayoutParams layoutParams = (ParallaxLayoutParams) params;
if(needParallax(layoutParams)){
//
ParallaxFrameLayout frameLayout = new ParallaxFrameLayout(getContext());
frameLayout.addView(child);
frameLayout.setParallaxAlpha(layoutParams.parallaxAlpha);
frameLayout.setParallaxFromBgColor(layoutParams.parallaxFromBgColor);
frameLayout.setParallaxToBgColor(layoutParams.parallaxToBgColor);
frameLayout.setParallaxScaleX(layoutParams.parallaxScaleX);
frameLayout.setParallaxScaleY(layoutParams.parallaxScaleY);
frameLayout.setParallaxTranslation(layoutParams.parallaxTranslation);
super.addView(frameLayout,params);
}else{
super.addView(child, params);
}
}
/**
* 在addView之前执行
* 重写该方法 在addView之前获取自定义属性
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new ParallaxLayoutParams(getContext(),attrs);
}
//判断是否含有视差效果 进而判断是否需要进行处理
private boolean needParallax(ParallaxLayoutParams layoutParams){
return layoutParams.parallaxAlpha ||
layoutParams.parallaxScaleX ||
layoutParams.parallaxScaleY ||
(layoutParams.parallaxTranslation != -1) ||
(layoutParams.parallaxFromBgColor != -1 && layoutParams.parallaxToBgColor != -1);
}
public class ParallaxLayoutParams extends LinearLayout.LayoutParams{
/**
*
* @param c
* @param attrs
*/
private boolean parallaxAlpha = false;//是否需要设置透明度的渐变 默认设置为false(不需要)
private int parallaxTranslation = -1;//是否需要设置平移动画 默认设置为-1(不需要)
private int parallaxFromBgColor = -1;//是否需要设置颜色的渐变 默认设置为-1(不需要)
private int parallaxToBgColor = -1;//是否需要设置颜色的渐变 默认设置为-1(不需要)
private boolean parallaxScaleX = false;//是否需要设置X轴的缩放 默认设置为false(不需要)
private boolean parallaxScaleY = false;//是否需要设置Y轴的缩放 默认设置为false(不需要)
public ParallaxLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
//解析我们的自定义属性
TypedArray parallaxArray = c.obtainStyledAttributes(attrs,R.styleable.parallax_attrs);
parallaxAlpha = parallaxArray.getBoolean(R.styleable.parallax_attrs_parallax_alpha,parallaxAlpha);
parallaxTranslation = parallaxArray.getInt(R.styleable.parallax_attrs_parallax_translation,parallaxTranslation);
parallaxFromBgColor = parallaxArray.getColor(R.styleable.parallax_attrs_parallax_fromBgColor,parallaxFromBgColor);
parallaxToBgColor = parallaxArray.getColor(R.styleable.parallax_attrs_parallax_toBgColor,parallaxToBgColor);
parallaxScaleX = parallaxArray.getBoolean(R.styleable.parallax_attrs_parallax_scaleX,parallaxScaleX);
parallaxScaleY = parallaxArray.getBoolean(R.styleable.parallax_attrs_parallax_scaleY,parallaxScaleY);
parallaxArray.recycle();
}
}
}
自定义的frameLayout
public class ParallaxFrameLayout extends FrameLayout implements ParallaxInterface{
private boolean parallaxAlpha;
private int parallaxTranslation;
private int parallaxFromBgColor;
private int parallaxToBgColor;
private boolean parallaxScaleX;
private boolean parallaxScaleY;
private int height;
private int width;
private int fromLeft = 0x01;
private int fromTop = 0x02;
private int fromRight = 0x04;
private int fromBottom = 0x08;
//颜色估值器
private ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();
public ParallaxFrameLayout(@NonNull Context context) {
super(context);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.width = w;
this.height = h;
}
public void setParallaxAlpha(boolean parallaxAlpha) {
this.parallaxAlpha = parallaxAlpha;
}
public void setParallaxTranslation(int parallaxTranslation) {
this.parallaxTranslation = parallaxTranslation;
}
public void setParallaxFromBgColor(int parallaxFromBgColor) {
this.parallaxFromBgColor = parallaxFromBgColor;
}
public void setParallaxToBgColor(int parallaxToBgColor) {
this.parallaxToBgColor = parallaxToBgColor;
}
public void setParallaxScaleX(boolean parallaxScaleX) {
this.parallaxScaleX = parallaxScaleX;
}
public void setParallaxScaleY(boolean parallaxScaleY) {
this.parallaxScaleY = parallaxScaleY;
}
private boolean getTranslationFrom(int value){
if(value == -1){
return false;
}
return (parallaxTranslation & value) == value;
}
/**
*
* @param ratio
*/
@Override
public void onShowParallax(float ratio) {
//alpha动画
if(parallaxAlpha){
setAlpha(ratio);
}
//背景的颜色渐变
if(parallaxFromBgColor != -1 && parallaxToBgColor != -1){
setBackgroundColor((Integer) mArgbEvaluator.evaluate(ratio,parallaxFromBgColor,parallaxToBgColor));
}
//设置X轴缩放
if(parallaxScaleX){
setScaleX(ratio);
}
//设置Y轴缩放
if(parallaxScaleY){
setScaleY(ratio);
}
//设置平移动画
if(getTranslationFrom(fromTop)){
setTranslationY(-height*(1-ratio));
}
if(getTranslationFrom(fromBottom)){
setTranslationY(height*(1-ratio));
}
if(getTranslationFrom(fromLeft)){
setTranslationX(-width*(1-ratio));
}
if(getTranslationFrom(fromRight)){
setTranslationX(width*(1-ratio));
}
}
@Override
public void onRestoreParallax() {
//alpha动画
if(parallaxAlpha){
setAlpha(0);
}
//背景的颜色渐变
if(parallaxFromBgColor != -1 && parallaxToBgColor != -1){
setBackgroundColor((Integer) mArgbEvaluator.evaluate(0,parallaxFromBgColor,parallaxToBgColor));
}
//设置X轴缩放
if(parallaxScaleX){
setScaleX(0);
}
//设置Y轴缩放
if(parallaxScaleY){
setScaleY(0);
}
//设置平移动画
if(getTranslationFrom(fromTop)){
setTranslationY(-height);
}
if(getTranslationFrom(fromBottom)){
setTranslationY(height);
}
if(getTranslationFrom(fromLeft)){
setTranslationX(-width);
}
if(getTranslationFrom(fromRight)){
setTranslationX(width);
}
}
}
定义一个接口,用来回调当前的状态(Ratio)
public interface ParallaxInterface {
//执行视差动画
public void onShowParallax(float ratio);
//状态复原
public void onRestoreParallax();
}
最后,在我们的例子中,由于需要一个上下滑动,根据滑动来执行相应的动画,所以我们再来自定义一个ScrollView:
public class ParallaxScrollView extends ScrollView {
private ParallaxLinearLayout mContent;
public ParallaxScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = (ParallaxLinearLayout) getChildAt(0);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//为了页面美观 将第一个子View的高设置为ScrollView的高度(覆盖全屏)
View firstView = mContent.getChildAt(0);
firstView.getLayoutParams().height = getHeight();
Log.i("justh","viewHeight--->"+firstView.getLayoutParams().height+"scrollViewHeight------->"+getHeight());
invalidate();
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//计算ratio = 当前控件显示高度/控件高度
int scrollViewHeight = getHeight();
for(int i = 0;i<mContent.getChildCount();i++){
View child = mContent.getChildAt(i);
int childHeight = child.getHeight();
if(!(child instanceof ParallaxInterface)){
continue;
}
ParallaxInterface parallaxInterface = (ParallaxInterface) child;
//获取child距离父容器顶部的距离
int childTop = child.getTop();
//t为先屏幕外滑动了多少
int absoluteTop = childTop - t;
if(absoluteTop <= scrollViewHeight){
//获取到child的可见距离
int visibleGap = scrollViewHeight - absoluteTop;
//计算除child的可见距离的百分比
float ratio = visibleGap/(float) childHeight;
//根据百分比执行动画
parallaxInterface.onShowParallax(clamp(ratio,1f,0f));
}else{
//恢复动画
parallaxInterface.onRestoreParallax();
}
}
}
//求三个数的中间大小的一个数。
public static float clamp(float value, float max, float min){
return Math.max(Math.min(value, max), min);
}
}
还有布局:
<?xml version="1.0" encoding="utf-8"?>
<com.example.dell.parallaxanimation.ParallaxScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:parallax="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.dell.parallaxanimation.MainActivity">
<com.example.dell.parallaxanimation.ParallaxLinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="700dp"
android:scaleType="centerCrop"
android:src="@mipmap/tb_bg" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#007788"
android:textColor="@android:color/black"
android:textSize="25sp"
android:padding="25dp"
android:gravity="center"
android:fontFamily="serif"
android:text="带上您的行李箱,准备shopping!"
parallax:parallax_alpha="true"
/>
<ImageView
android:layout_width="200dp"
android:layout_height="120dp"
android:layout_gravity="top|right"
parallax:parallax_translation="fromLeft|fromBottom"
parallax:parallax_alpha="true"
android:src="@mipmap/baggage" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:textColor="@android:color/black"
android:textSize="25sp"
android:padding="25dp"
android:gravity="center"
android:fontFamily="serif"
android:text="准备好相机,这里有你想象不到的惊喜!"
parallax:parallax_fromBgColor="#ffff00"
parallax:parallax_toBgColor="#88EE66"
/>
<ImageView
android:layout_width="220dp"
android:layout_height="110dp"
android:layout_gravity="right"
android:src="@mipmap/camera"
parallax:parallax_translation="fromRight" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:fontFamily="serif"
android:gravity="center"
android:background="#D97C1F"
android:text="这次淘宝造物节真的来了,我们都在造,你造吗?\n7月22日-7月24日\n上海世博展览馆\n在现场,我们造什么?"
android:textSize="23sp"
parallax:parallax_alpha="true"
parallax:parallax_translation="fromBottom" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/sweet"
parallax:parallax_scaleX="true"
parallax:parallax_scaleY="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/shoes"
parallax:parallax_alpha="true"
parallax:parallax_scaleX="true"
parallax:parallax_scaleY="true"
parallax:parallax_translation="fromLeft|fromBottom" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/shoes"
parallax:parallax_alpha="true"
parallax:parallax_scaleY="true"
parallax:parallax_translation="fromRight|fromTop" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/sweet"
parallax:parallax_alpha="true"
parallax:parallax_scaleY="true"
parallax:parallax_translation="fromLeft" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/camera"
parallax:parallax_scaleY="true"
parallax:parallax_translation="fromLeft" />
</com.example.dell.parallaxanimation.ParallaxLinearLayout>
</com.example.dell.parallaxanimation.ParallaxScrollView>
自定义属性:
<declare-styleable name="parallax_attrs">
<!-- 透明度 -->
<attr name="parallax_alpha" format="boolean"/>
<!-- 平移 -->
<attr name="parallax_translation"/>
<!-- 颜色渐变 -->
<attr name="parallax_fromBgColor" format="color"/>
<attr name="parallax_toBgColor" format="color"/>
<!-- 缩放 -->
<attr name="parallax_scaleX" format="boolean"/>
<attr name="parallax_scaleY" format="boolean"/>
</declare-styleable>
<attr name="parallax_translation">
<flag name="fromLeft" value="0x01"/>
<flag name="fromTop" value="0x02"/>
<flag name="fromRight" value="0x04"/>
<flag name="fromBottom" value="0x08"/>
</attr>
好了 到这里呢,差不多就结束了,但最后想记录一个小知识点:
在我们的自定义属性中,有如下的赋值情况:
parallax:parallax_translation="fromLeft|fromBottom"
我们在代码中,同事加入了两个值,fromLeft和fromBottom
这个在我们自定义View的解析属性中,我们需要如何来获取其含有哪些值呢?
首先:
我的解决思路是:1:我们的自定义属性的值,都给其设置为2的几次幂,这样换算成二进制就是: 00000001
00000010
00000100
00001000
在我们设置自定义属性值的时候,我们使用了|(或)运算,所以在我们获得的值,也是经过或运算得到的值,后面,我们只需要使用原值(如上面提到的00000001)与获取到的值进行与运算,当结果与原值相等时,就说明我们设置了该属性,反之则没有。