Android动画实战-实现下拉式窗帘动画和上拉式抽屉动画
来源:互联网 发布:神武2mac版 编辑:程序博客网 时间:2024/04/27 19:31
最近比较忙,因为月底App要上线。但是忙里也得偷闲丫~哈哈!!
在上篇博客中,我和大家分享了如何使用Android动画来实现高仿简聊App中菜单动画的效果。还木有看的小伙伴赶紧的哟:Android动画实战一仿简聊App动画菜单
今天,我将和大家分享我们Android动画之旅系列的最后一个实战项目:下拉式窗帘动画和上拉式抽屉动画。目前这种动画效果在App中也是比较流行的,ok,我们一起先来看看效果:
上面展示了两张效果图,第一张是点击TopBar上的分享按钮,分享的布局以窗帘式的动画效果徐徐向下滑出。第二张是点击TopBar上的分享按钮,布局以抽屉式的动画效果徐徐向上滑出,是不是超级赞!分享完了动画效果,下面,我将和大家一起来实现这样的Android动画。
在实现动画之前,我们先来分析一下具体的实现流程:
首先,在我们的布局中有几个分享的选择按钮:微博、QQ、朋友圈等等...当点击TopBar上的按钮时,需要让布局开启一个渐渐向下或向上的动画,直到向下或向上移动的高度为布局高度时,动画停止。点击布局中的按钮,进行分享等操作,然后点击布局周围的区域,让布局消失。所以从技术角度来说,我们需要选择的Android动画是:ObjectAnimator,没错,就是属性动画!其实,这种效果不仅可以使用动画来实现,同时呢,Google在Android中还为我们提供了Scroller类。还记得吗?这个Scroller也是为了实现渐渐的动画效果而存在的。关于Scroller,本篇博客就不再细讲了,有兴趣的小伙伴可以了解下。
分析完动画的实现原理,那么就开始我们今天的主题吧!
首先,来看最终的效果图:
当我们点击TopBar右端分享按钮时,黄色区域的布局就会从顶部渐渐展开,当我们点击中间的关闭按钮时,又会渐渐向上回收。首先来看布局文件:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <!--下拉--> <RelativeLayout android:id="@+id/rl_share" android:layout_width="match_parent" android:layout_height="200dp" android:background="@android:color/holo_orange_light" android:orientation="vertical" android:layout_marginTop="-200dp" android:gravity="center" android:layout_below="@+id/topbar" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_orange_light" android:orientation="horizontal" android:layout_marginTop="46dp" android:gravity="center" > <ImageView android:id="@+id/iv_qq" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_qq_checked" /> <ImageView android:id="@+id/iv_qzone" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_qzone_checked" /> <ImageView android:id="@+id/iv_wechat" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_wechat_checked" /> <ImageView android:id="@+id/iv_moments" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_wechatmoments_checked" /> </LinearLayout> <ImageButton android:id="@+id/ibtn_close" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="center" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="12dp" android:background="@drawable/ssdk_oks_img_cancel" /> </RelativeLayout> <RelativeLayout android:id="@+id/rl_bottom_share" android:layout_width="match_parent" android:layout_height="200dp" android:background="@android:color/holo_orange_light" android:orientation="vertical" android:layout_marginBottom="-200dp" android:layout_alignParentBottom="true" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_orange_light" android:orientation="horizontal" android:layout_marginTop="46dp" android:gravity="bottom" android:layout_centerInParent="true" > <ImageView android:id="@+id/iv_bottom_qq" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_qq_checked" /> <ImageView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_qzone_checked" /> <ImageView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_wechat_checked" /> <ImageView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:src="@drawable/ssdk_oks_skyblue_logo_wechatmoments_checked" /> </LinearLayout> <ImageButton android:id="@+id/ibtn_bottom_close" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="center" android:layout_marginRight="20dp" android:layout_alignParentRight="true" android:layout_marginTop="20dp" android:layout_centerHorizontal="true" android:layout_marginBottom="12dp" android:background="@drawable/ssdk_oks_img_cancel" /> </RelativeLayout> <RelativeLayout android:id="@+id/topbar" android:layout_width="match_parent" android:layout_height="46dp" android:background="@android:color/holo_red_light" > <ImageButton android:id="@+id/ibtn_top_share" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="center" android:background="@drawable/nav_share" android:layout_centerVertical="true" android:layout_alignParentLeft="true" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/app_title" android:textSize="18dp" android:textColor="@android:color/white" android:layout_centerInParent="true" /> <ImageButton android:id="@+id/ibtn_bottom_share" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="center" android:background="@drawable/nav_share" android:layout_centerVertical="true" android:layout_alignParentRight="true" /> </RelativeLayout></RelativeLayout>
从上图代码中,我们看到,根布局采用了RelativeLayout布局,大家都知道,在RelativeLayout中定义的子View顺序不同,覆盖层就不同,先定义的在下面,后定义的会覆盖上面的布局。好了,布局文件其实很简单,就是几个ImageView+ImageButton来实现。这里就不再具体解释了。我们有了布局文件之后,就可以用属性动画ofFloat来设置在Y轴方向的位移动画,并将该动画执行在黄色区域也就是我们的分享框的布局中。此时,设置分享按钮的单击事件,开启动画,就可以轻松搞定啦!下面,来看核心的几段代码:
/** * 初始化View */private void initView() { ivQQ = (ImageView) findViewById(R.id.iv_qq); ibtnTopShare = (ImageButton) findViewById(R.id.ibtn_top_share); ibtnClose = (ImageButton) findViewById(R.id.ibtn_close); rlShare = (RelativeLayout) findViewById(R.id.rl_share); ibtnBottomClose = (ImageButton) findViewById(R.id.ibtn_bottom_close); ibtnBottomShare = (ImageButton) findViewById(R.id.ibtn_bottom_share); rlBottomShare = (RelativeLayout) findViewById(R.id.rl_bottom_share); //解决在onCreate中不能获取高度的问题 rlShare.post(new Runnable() { @Override public void run() { rlTopShareHeight = rlShare.getHeight(); initAnimation(); } });}
在上面代码中,我们调用了分享框布局的post去启动一个Runnable线程,在run方法中,我们去获取布局的高度(因为我们要让分享框下拉的高度就是它本身布局的高度)。为什么要这么写呢?知道View绘制流程的小伙伴应该都知道,在Activity的onCreate方法执行时,View的onMeasure,onLayout方法都是还没有执行的。只有当Window建立完毕,View绘制完成时才可以获取高度。此时我们用post就是为了在该布局绘制、布局完成后来获取该布局的高度。并根据高度初始化动画效果,下面来看initAnimation()这个方法:
/** * 初始化Animation */private void initAnimation() { /** * 顶部动画 */ //打开动画 topPullAnimation = ObjectAnimator.ofFloat( rlShare,"translationY",rlTopShareHeight); topPullAnimation.setDuration(1000); topPullAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); //关闭动画 topUpAnimation = ObjectAnimator.ofFloat( rlShare,"translationY",-rlTopShareHeight); topUpAnimation.setDuration(500); topUpAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); topUpAnimation.start(); /** * 底部动画 */ //打开动画 bottomUpAnimation = ObjectAnimator.ofFloat( rlBottomShare, "translationY", -rlTopShareHeight); topUpAnimation.setDuration(500); topUpAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); topUpAnimation.start(); //关闭动画 bottomPullAnimation = ObjectAnimator.ofFloat( rlBottomShare, "translationY", rlTopShareHeight); topPullAnimation.setDuration(1000); topPullAnimation.setInterpolator(new AccelerateDecelerateInterpolator());}
上面这段代码就是初始化动画并设置动画效果。很简单,就是用了translationY在Y轴的移动。此时,小伙伴们要记住一点:如果要向下移动,Y的值应该是正,否则为负值。然后,我们就可以通过在按钮的单击事件中去启动对应的动画,就可以啦:
/** * 单击事件 * @param v */@Overridepublic void onClick(View v) { switch(v.getId()) { case R.id.ibtn_top_share: //click share btn if(!topPullAnimation.isRunning()) { topPullAnimation.start(); } break; case R.id.ibtn_close: //click close btn if(!topUpAnimation.isRunning()) { topUpAnimation.start(); } break; case R.id.ibtn_bottom_share: //click close btn if(!bottomUpAnimation.isRunning()) { bottomUpAnimation.start(); } break; case R.id.ibtn_bottom_close: //click close btn if(!bottomPullAnimation.isRunning()) { bottomPullAnimation.start(); } break; default: break; }}
OK,一个看起来复杂的动画,是不是在属性动画面前就变的特别简单了!下面是从底部弹出,其实唯一不同的就是Y轴的移动方向的改变。会了从顶部向下弹出,对于从底部向上弹出,小伙伴们肯定都已经知道该怎么实现了,这里就不再细说。
之前在群里有人说想知道用Scroller来如何实现该动画效果,Scroller是android.widget下的一个类,使用该该可以做渐渐的动画效果。下面我就补充上使用Scroller来如何实现该动画。
要使用Scroller来实现,我们就需要自定义一个View。因为这个View就是个Viewgroup,里面放了几个按钮。所以我们让自定义的View继承一个RelativeLayout布局。
下面来看我们需要的变量:
private Scroller mScroller;//scroller拖动类private int mScreenHeight;//屏幕高度private boolean isMoving;//是否还在移动private int mViewHeight = 0;//布局的高度private boolean isShow = false;//是否打开private int mDuration = 1000;//执行动画时间
上面的声明变量是几个比较核心的,每个变量我都写了注释,大家一看就知道是什么意思了。下面来看初始化的代码:
/** * 初始化 * @param context */private void init(Context context) { //afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setFocusable(true); mScroller = new Scroller(context); mScreenHeight = ScreenUtils.getScreenHeight(context); //背景设置成透明 this.setBackgroundColor(Color.argb(0, 0, 0, 0)); final View view = LayoutInflater.from(context).inflate(R.layout.view_share_app, null); // 如果不给他设这个,它的布局的MATCH_PARENT就不知道该是多少 LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); this.addView(view, params); view.post(new Runnable() { @Override public void run() { mViewHeight = view.getHeight(); } }); this.scrollTo(0, mScreenHeight);}
在init方法中,我们首先初始化了Scroller对象。mScroller = new Scroller(context);
然后使用LayoutInflater加载了我们要使用的分享框的布局,并调用addView将该布局添加到RelativeLayout布局中。,下面又调用了view的post方法创建一个Runnable在run中获取布局的高度。上面我们已经分析过原因了,此处不多说。然后调用了View的scrollTo方法。scrollTo方法就是使该View初始化完毕后向下滚动到屏幕的高度,也就是在屏幕底部的下面。这样就可以将“隐藏"在屏幕的下面了。然后我们定义一个打开和关闭的方法,并将其定义成public的,供我们在Activity等界面中控制布局的打开和关闭:
/**打开界面**/public void show(){ if(!isShow && !isMoving){ this.startMoveAnim(-mViewHeight, mViewHeight, mDuration); isShow = true; }}/**关闭界面**/public void dismiss(){ if(isShow && !isMoving){ this.startMoveAnim(0, -mViewHeight, mDuration); isShow = false; change(); }}
可以看到,我们在show和dismiss方法中都调用了startMoveAnim方法,来看这个方法:/** * 拖动动画 * @param startY * @param dy 移动到某点的Y坐标距离 * @param duration 时间 */private void startMoveAnim(int startY, int dy, int duration) { isMoving = true; mScroller.startScroll(0, startY, 0, dy, duration); invalidate();//通知UI线程的更新 }
该方法由三个参数,第一个是开始的Y坐标,第二个是要移动的距离,第三个是执行所需要的时间。然后我们调用Scroller的startScroll方法将值传入:startScroll有两个重载,我们从上图可以看到,第一个参数是开始的X坐标,第二个是开始的Y坐标,第三个是X方向移动的距离,第四个是Y方向移动的距离,最后一个就是动画的执行时间,查看startScroll的源码可以看到:
/** * Start scrolling by providing a starting point and the distance to travel. * The scroll will use the default value of 250 milliseconds for the * duration. * * @param startX Starting horizontal scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param startY Starting vertical scroll offset in pixels. Positive numbers * will scroll the content up. * @param dx Horizontal distance to travel. Positive numbers will scroll the * content to the left. * @param dy Vertical distance to travel. Positive numbers will scroll the * content up. */public void startScroll(int startX, int startY, int dx, int dy) { startScroll(startX, startY, dx, dy, DEFAULT_DURATION);}
如果我们不传入执行时间,系统会默认有一个DEFAULT_DURATION的执行时间:250毫秒。private static final int DEFAULT_DURATION = 250;
介绍完了startScroll方法,我们继续看startMoveAnim方法,后面我们调用了invalidate()方法,此处我来解释一下:
因为当我们调用Scroller的startScroll()方法并将值传入后,其实Scroller并没有做出移动事件,即调用startScroll方法只是去初始化滚动的值,此时需要我们去调用invalidate来驱动Scroller来做出滚动效果,此处我们从源代码也可以看得出来:
/** * Start scrolling by providing a starting point, the distance to travel, * and the duration of the scroll. * * @param startX Starting horizontal scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param startY Starting vertical scroll offset in pixels. Positive numbers * will scroll the content up. * @param dx Horizontal distance to travel. Positive numbers will scroll the * content to the left. * @param dy Vertical distance to travel. Positive numbers will scroll the * content up. * @param duration Duration of the scroll in milliseconds. */public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration;}
调用invalidate来驱动Scroller来做出滚动效果的方法就是computeScroll方法,即当我们调用了invalidate时,Scoller的startScroll方法需要结合computeScroll来一起完成滚动效果:/** * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} * object. */public void computeScroll() {}
@Overridepublic void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 更新界面 postInvalidate(); isMoving = true; } else { isMoving = false; } super.computeScroll();}
上面代码就是我们自己重写了computeScroll方法,在该方法中,我们去调用computeScrollOffset方法先去判断滚动是否结束。下面是这个方法的源码:/** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE: final float t = (float) timePassed / mDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); if (mCurrX == mFinalX && mCurrY == mFinalY) { mFinished = true; } break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true;}
很简单,也就是去根据设置的初始值来判断是否已经移动完毕。从注释也能看出,如果该方法返回true,此时动画还没有结束。继续看我们的computeScroll方法,判断完毕后,如果动画执行没有完毕,我们调用了scrollTo方法来做真正的滚动,scrollTo接收了两个值,看源码:
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } }}
x,y也就是我们要滚动到的位置。此时我们通过Scroller的getCurrX()和getCurrY()方法来获取Scroller的当前值作为参数传递给scrollTo方法来做滚动动画。最后我们还需要调用postInvalidate()方法,调用该方法后,系统会重新回调draw的方法去重新绘制,即又会执行computeScroll方法,形成一个循环,直到动画结束。这也就是为什么我们可以使用Scroller来实现循序渐进的动画效果。
最后我们再来看一下postInvalidate的源码:
/** * <p>Cause an invalidate to happen on a subsequent cycle through the event loop. * Use this to invalidate the View from a non-UI thread.</p> * * <p>This method can be invoked from outside of the UI thread * only when this View is attached to a window.</p> * * @see #invalidate() * @see #postInvalidateDelayed(long) */public void postInvalidate() { postInvalidateDelayed(0);}
在postInvalidate方法中又调用了postInvalidateDelayed(0)方法。/** * <p>Cause an invalidate to happen on a subsequent cycle through the event * loop. Waits for the specified amount of time.</p> * * <p>This method can be invoked from outside of the UI thread * only when this View is attached to a window.</p> * * @param delayMilliseconds the duration in milliseconds to delay the * invalidation by * * @see #invalidate() * @see #postInvalidate() */public void postInvalidateDelayed(long delayMilliseconds) { // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds); }}
看到了吗!!在postInvalidateDelayed方法中又通过viewrootImpl去调用dispatchInvaludateDelayed方法:public void dispatchInvalidateDelayed(View view, long delayMilliseconds) { Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view); mHandler.sendMessageDelayed(msg, delayMilliseconds);}
dispatchInvaludateDelayed方法调用了Handler发送更新界面的消息,让系统更新UI。到目前为止,我们对于该动画的实现就结束了,本篇博客,我带大家一起分别用属性动画和使用Scoller实现拉式窗帘动画和上拉式抽屉动画效果,并从源代码角度分析了Scroller的原理。博文有点凌乱还希望大家多多包含,下一篇,我将和大家一起对Android动画系列做一个小小的总结。
最后附上本篇项目的源码链接:实现下拉式窗帘动画和上拉式抽屉Android动画
- Android动画实战-实现下拉式窗帘动画和上拉式抽屉动画
- Android 下拉展开动画
- android动画,Tween动画和Frame动画
- Android 动画实战
- Android属性动画实战
- Android动画之帧动画,及实现京东下拉加载动画
- Android 动画-->灵动菜单、计时器动画、下拉展开动画
- Android动画实战-仿简聊App动画菜单
- 最流行抽屉动画
- Android launcher 桌面抽屉切换动画
- Swift下拉菜单动画实现
- MJRefresh实现动画下拉刷新
- Android的下拉刷新动画
- android下拉刷新精彩动画
- jQuery动画-上卷和下拉
- Android动画详解之Android 动画属性和实现方法之帧动画(二)
- Android动画详解之Android 动画属性和实现方法之属性动画(三)
- Android 动画机制及实战
- PHPMailer使用
- iOS 微信第三方登录的简单实现
- 初探Hibernate
- VR
- KAFKA学习总结
- Android动画实战-实现下拉式窗帘动画和上拉式抽屉动画
- androidapk安装过程详解
- iOS 单利的简单创建
- Hibernate实体映射
- JSON Views 高级用法
- MAC通用终端(terminal)光标的移动
- Java经典设计模式(2):七大结构型模式(附实例和详解)
- fatal error C1083: 无法打开预编译头文件 正确解法
- Android ListView默认选中某一项