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) {    //afterDescendantsviewgroup只有当其子类控件不需要获取焦点时才获取焦点    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动画 



2 0
原创粉丝点击