Custom Animations With Fragments

最近在看Cyril Mottier(公认的安卓开发大牛)大神的博客的时候看到了一篇文章,是介绍他们公司研发的CapitaineTrain(欧洲一款很受欢迎的铁路购票客户端)项目中实现的一个比较酷的效果。先上两张效果图

效果是不是很酷!从GooglePlay下载了CapitaineTrain安装后看了下操作,整个软件非常简洁,操作性特别强,人性化,动画流畅,比我国某铁的购票客户端强的太多了。是不是迫不及待的想实现这个效果?那就跟着我一起来做~

这个是原博客地址:http://cyrilmottier.com/2014/05/20/custom-animations-with-fragments/

起初只是想简单的实现下动画切换的效果,但是使用过Cyril大神开发的CapitaineTrain后感觉界面太清爽了,感叹安卓也能做出如此流畅的动画(这里肯定会有很多人说我太菜了~~~)!我这个人有个缺点,就是每当看到一个软件某个效果很好时就迫不及待的想去实现,另外说下我这个人有强迫症,如果是模仿的话也要尽量跟原客户端保持一致的效果,这款软件也不例外~花了一个多星期终于实现了自己想要的效果。由于我机器的安卓模拟器太卡了也就没有录制gif,跟上面的效果差不多。下面是我做的一个效果,只是静态图

我的实现步骤是这样的:首先是软件顶部导航的实现,然后是导航栏下面的布局实现,最后是动画的实现。
1、导航栏的实现
导航栏采用的是ActionBar来实现的,之前一直都用的自定义LinearLayout或者RelativeLayout来实现的,试着来实现一把。

其实5.0版本之后推出了ToolBar这个控件,跟ActionBar差不多,这里就不作介绍了。

导航栏分为这几个部分:导航栏和状态栏的颜色、导航栏左边的三个图标、图标下方的指示条、右边的Menu按钮。

(1)导航栏和状态栏的颜色

下面是我反编译CapitaineTrain后拿出来的资源文件(提起反编译感觉有点不太厚道):

<style name="Base.Theme.CapitaineTrain" parent="@style/Theme.AppCompat.Light.DarkActionBar">
    ......
    <item name="colorPrimary">@color/ct_green</item>
    <item name="colorPrimaryDark">@color/ct_dark_green</item>
    ......
</style>

colorPrimary属性定义了导航栏的颜色,colorPrimaryDark是5.0以后Material Design才有的属性,请看下面一张图

写好style之后别忘了在Manifest中应用该样式

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Base.Theme.CapitaineTrain" >
        <activity
            android:name="com.ldw.capitainetrain.ui.MainActivity"
            android:label="@string/app_name"
            android:theme="@style/Theme.CapitaineTrain.EmptyActionBar.NoInset" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

至于关于MaterialDesign的一些设计大家可以参考http://www.google.com/design/spec/style/color.html#

(2)导航栏左边三个icon和底部指示条的实现

本来是想自己修改下PagerSlidingTabStrip这个开源控件,但在github上无意中看到了https://github.com/Mirkoddd/TabBarView这个开源控件就直接拿过来用了,但是我也做了些许修改,包括指示条的高度,去掉icon中间的分割线,icon选中时的状态标识,icon长按时的Toast提示。下面上代码:

TabBarView类

package com.ldw.capitainetrain.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.LinearLayout;

import com.ldw.capitainetrain.R;

public class TabBarView extends LinearLayout {
    public interface IconTabProvider {
        public int getPageIconResId(int position);
    }

    private static final int STRIP_HEIGHT = 2;

    public final Paint mPaint;

    private int mStripHeight;
    private float mOffset = 0f;
    public static int mSelectedTab = 0;
    public ViewPager pager;

    public static int tabCount;
    private final PageListener pageListener = new PageListener();
    public OnPageChangeListener delegatePageListener;

    private TabView child;

    private View nextChild;

    public static int a;

    private Context mContext;

    private int mToolBarHeight;

    public TabBarView(Context context) {
        this(context, null);
    }

    public TabBarView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.actionBarTabBarStyle);
    }

    public TabBarView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mContext = context;

        setWillNotDraw(false);

        mPaint = new Paint();
        mPaint.setColor(Color.WHITE);
        mPaint.setAntiAlias(true);

        mStripHeight = (int) (STRIP_HEIGHT
                * getResources().getDisplayMetrics().density + .5f);

        mToolBarHeight = context.getResources().getDimensionPixelSize(
                R.dimen.toolbar_height);
    }

    public void setStripColor(int color) {
        if (mPaint.getColor() != color) {
            mPaint.setColor(color);
            invalidate();
        }
    }

    public void setStripHeight(int height) {
        if (mStripHeight != height) {
            mStripHeight = height;
            invalidate();
        }
    }

    public void setSelectedTab(int tabIndex) {
        if (tabIndex < 0) {
            tabIndex = 0;
        }
        final int childCount = getChildCount();
        if (tabIndex >= childCount) {
            tabIndex = childCount - 1;
        }
        if (mSelectedTab != tabIndex) {
            mSelectedTab = tabIndex;
            invalidate();
        }
    }

    public void setOffset(int position, float offset) {
        if (mOffset != offset) {
            mOffset = offset;
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Draw the strip manually
        child = (TabView) getChildAt(mSelectedTab);
        int height = getHeight();
        if (child != null) {
            float left = child.getLeft();
            float right = child.getRight();
            if (mOffset > 0f && mSelectedTab < tabCount - 1) {
                nextChild = getChildAt(mSelectedTab + 1);
                if (nextChild != null) {
                    final float nextTabLeft = nextChild.getLeft();
                    final float nextTabRight = nextChild.getRight();
                    left = (mOffset * nextTabLeft + (1f - mOffset) * left);
                    right = (mOffset * nextTabRight + (1f - mOffset) * right);
                }
            }
            canvas.drawRect(left, height - mStripHeight, right, height, mPaint);
        }
    }

    public void setViewPager(ViewPager pager) {
        this.pager = pager;

        if (pager.getAdapter() == null) {
            throw new IllegalStateException(
                    "ViewPager does not have adapter instance.");
        }

        pager.setOnPageChangeListener(pageListener);

        notifyDataSetChanged();
    }

    private class PageListener implements OnPageChangeListener {

        @Override
        public void onPageScrolled(int position, float positionOffset,
                int positionOffsetPixels) {

            mSelectedTab = position;
            mOffset = positionOffset;

            invalidate();

            if (delegatePageListener != null) {
                delegatePageListener.onPageScrolled(position, positionOffset,
                        positionOffsetPixels);
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            if (state == ViewPager.SCROLL_STATE_IDLE) {

            }

            if (delegatePageListener != null) {
                delegatePageListener.onPageScrollStateChanged(state);
            }
        }

        @Override
        public void onPageSelected(int position) {
            if (delegatePageListener != null) {
                delegatePageListener.onPageSelected(position);
            }

            changeIconAlpha(position);

        }
    }

    public void notifyDataSetChanged() {

        this.removeAllViews();

        tabCount = pager.getAdapter().getCount();

        for (int i = 0; i < tabCount; i++) {

            if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {

                addTabViewP(i, pager.getAdapter().getPageTitle(i).toString(),
                        ((IconTabProvider) pager.getAdapter())
                                .getPageIconResId(i));

            } else {

                addTabViewL(i, pager.getAdapter().getPageTitle(i).toString(),
                        ((IconTabProvider) pager.getAdapter())
                                .getPageIconResId(i));

            }
        }

        getViewTreeObserver().addOnGlobalLayoutListener(
                new OnGlobalLayoutListener() {

                    @SuppressLint("NewApi")
                    @Override
                    public void onGlobalLayout() {

                        getViewTreeObserver()
                                .removeOnGlobalLayoutListener(this);

                        mSelectedTab = pager.getCurrentItem();
                        //更改icon的alpha值
                        changeIconAlpha(mSelectedTab);

                    }
                });

    }

    private void changeIconAlpha(int position) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            TabView tabView = (TabView) getChildAt(i);
            float alpha = 0;
            if (position == i) {
                alpha = 1.0f;
            } else {
                alpha = 0.75f;
            }
            tabView.setIconAlpha(alpha);
        }
    }

    private void addTabViewL(final int i, String string, int pageIconResId) {
        // TODO Auto-generated method stub
        TabView tab = new TabView(getContext());
        // tab.setIcon(pageIconResId);
        tab.setText(string, pageIconResId);
        tab.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                pager.setCurrentItem(i);
            }
        });

        this.addView(tab);
    }

    @SuppressLint("NewApi")
    private void addTabViewP(final int i, final String string, int pageIconResId) {
        // TODO Auto-generated method stub
        final TabView tab = new TabView(getContext());
        tab.setContentDescription(string);
        tab.setIcon(pageIconResId);
        if (android.os.Build.VERSION.SDK_INT >= 5) {
//			int[] attrs = new int[] { android.R.attr.selectableItemBackground };
//			TypedArray ta = mContext.obtainStyledAttributes(attrs);
//			Drawable drawableFromTheme = ta.getDrawable(0);
//			ta.recycle();
//			tab.setBackground(drawableFromTheme);

            tab.setBackgroundResource(R.drawable._selector_dark_borderless);
        }
        tab.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {

                pager.setCurrentItem(i);
            }
        });
//		CheatSheet.setup(tab, string);

        this.addView(tab, new LayoutParams(LayoutParams.WRAP_CONTENT,
                mToolBarHeight));
    }

    public void setOnPageChangeListener(OnPageChangeListener listener) {
        this.delegatePageListener = listener;
    }

}

tab.setBackgroundResource(R.drawable._selector_dark_borderless);这句话是我加上的5.0上面的ripple效果。

TabView类:

package com.ldw.capitainetrain.view;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.TextView;

public class TabView extends LinearLayout {

    private ImageView mImageView;
    private TextView mTextView;

    private OnLongClickListener mLongClickListener;

    public TabView(Context context) {
        this(context, null);
    }

    public TabView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.actionBarTabStyle);
    }

    public TabView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedValue outValue = new TypedValue();
        context.getTheme().resolveAttribute(android.R.attr.actionBarTabTextStyle, outValue, true);

        int txtstyle = outValue.data;

        int pad = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources()
                    .getDisplayMetrics());

        mImageView = new ImageView(context);
        mImageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
        mImageView.setScaleType(ScaleType.CENTER_INSIDE);

        mTextView = new TextView(context);
        mTextView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mTextView.setGravity(Gravity.CENTER);
        mTextView.setCompoundDrawablePadding(pad);
        mTextView.setTextAppearance(context, txtstyle);;

        this.addView(mImageView);
        this.addView(mTextView);
        this.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

        mLongClickListener = new OnLongClickToastListener(this);
    }

    public void setIcon(int resId) {
        setIcon(getContext().getResources().getDrawable(resId));

        setOnLongClickListener(mLongClickListener);
    }

    public void setIcon(Drawable icon) {
        if (icon != null) {
            mImageView.setVisibility(View.VISIBLE);
            mImageView.setImageDrawable(icon);
        } else {
            mImageView.setImageResource(View.GONE);
        }
    }

    public void setText(int resId, int ico) {
        setText(getContext().getString(resId), ico);
    }

    public void setText(CharSequence text, int ico) {
        mTextView.setText(text);
        mTextView.setCompoundDrawablesWithIntrinsicBounds(ico, 0, 0, 0);;
    }

    public void setIconAlpha(float alpha) {
    	mImageView.setAlpha(alpha);
    }

}

OnLongClickToastListener类:

/**
 * Copyright (c) www.longdw.com
*/
package com.ldw.capitainetrain.view;

import android.content.Context;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.widget.Toast;

public class OnLongClickToastListener implements OnLongClickListener {

    private TabView mTabView;

    public OnLongClickToastListener(TabView tabView) {
        mTabView = tabView;
    }

    @Override
    public boolean onLongClick(View v) {

        int[] location = new int[2];
        mTabView.getLocationOnScreen(location);
        Context context = mTabView.getContext();
        int width = mTabView.getWidth();
        int height = mTabView.getHeight();
        int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
        CharSequence cs = mTabView.getContentDescription();
        Toast toast = Toast.makeText(context, cs, Toast.LENGTH_SHORT);
        int x = location[0];

        //因为默认的Toast是出于屏幕中间位置
        int xOffset = x + width / 2 - screenWidth / 2; 

        toast.setGravity(49, xOffset, height);
        toast.show();
        return true;
    }

}

下面是程序的入口类MainActivity类:

package com.ldw.capitainetrain.ui;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;

import com.ldw.capitainetrain.R;
import com.ldw.capitainetrain.view.TabBarView;
import com.ldw.capitainetrain.view.TabBarView.IconTabProvider;

public class MainActivity extends ActionBarActivity {

    private ViewPager mViewPager;
    private SectionsPagerAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

//		Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
//		setSupportActionBar(toolbar);

        getSupportActionBar().setDisplayShowTitleEnabled(false);
        getSupportActionBar().setDisplayShowHomeEnabled(false);
        getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);

//		int toolBarHeight = getResources().getDimensionPixelSize(R.dimen.toolbar_height);

        TabBarView tbv = (TabBarView) getLayoutInflater().inflate(R.layout.toolbar_layout, null);
//		toolbar.addView(tbv, new LayoutParams(LayoutParams.WRAP_CONTENT, toolBarHeight));
        getSupportActionBar().setCustomView(tbv);

        mViewPager = (ViewPager) findViewById(R.id.viewpager);
        mAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
        mViewPager.setAdapter(mAdapter);

        tbv.setViewPager(mViewPager);
    }

    public class SectionsPagerAdapter extends FragmentPagerAdapter implements IconTabProvider{

        private int[] mDrawable = { R.drawable.ic_tab_search, R.drawable.ic_tab_cart, R.drawable.ic_tab_tickets };

        public SectionsPagerAdapter(FragmentManager fm) {
            super(fm);

        }

        @Override
        public Fragment getItem(int position) {
            return new SearchFragment();
        }

        @Override
        public int getCount() {
            // Show 3 total pages.
            return mDrawable.length;
        }

        @Override
        public int getPageIconResId(int position) {
            return mDrawable[position];
        }

        @Override
        public CharSequence getPageTitle(int position) {
            switch (position) {
            case 0:
                return "Search";
            case 1:
                return "Cart";
            case 2:
                return "Tickets";
            }
            return null;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

对了,在开发的时候遇到个问题,就是icon图标总是左边总是有距离

为了去掉这个间距,我们可以在style中添加如下代码:

<style name="Widget.ToolBar" parent="@style/Widget.AppCompat.Toolbar">
        <item name="maxButtonHeight">?actionBarSize</item>
</style>

<style name="Widget.ToolBar.NoInset" parent="@style/Widget.ToolBar">
        <item name="contentInsetStart">0.0dip</item>
</style>

<style name="Theme.CapitaineTrain.EmptyActionBar.NoInset" parent="@style/Theme.CapitaineTrain.EmptyActionBar">
        <item name="toolbarStyle">@style/Widget.ToolBar.NoInset</item>
</style>

主要是contentInsetStart属性。通过以上代码我们可以实现导航栏切换的效果。接下来我们来实现点击输入框时候的动画效果。

2、动画的实现

动画实现主要涉及到以下几个核心的知识点。

(1)通过ViewPropertyAnimator来实现动画;
(2)动画需要手动计算开始和结束的位置,先通过View#getDrawingRect(Rect)计算view在父坐标系中的位置,然后通过ViewGroup#offsetDescendantRectToMyCoords(View, Rect)来计算在根视图(按作者的说法是ancestor coordinate system)中的位置;
(3)通过AnimatorListenerAdapter来优化动画。

上代码,SearchFragment类

/**
 * Copyright (c) www.longdw.com
 */
package com.ldw.capitainetrain.ui;

import android.animation.TimeInterpolator;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.ldw.capitainetrain.R;
import com.ldw.capitainetrain.anim.CustomAnimator;
import com.ldw.capitainetrain.anim.LayerEnablingAnimatorListener;

public class SearchFragment extends Fragment implements OnClickListener {

    private TimeInterpolator ANIMATION_INTERPOLATOR = new DecelerateInterpolator();
    private int ANIMATION_DURATION = 500;
    private TextView mOutwardTv;
    private TextView mInwardTv;
    private FrameLayout mMainContainer;

    private LinearLayout mDateTimeContainer;
    private LinearLayout mStationsContainer;
    private FrameLayout mFirstSpacer;
    private FrameLayout mSencondSpacer;

    private LinearLayout mPassengersContainer;
    private FrameLayout mThirdSpacer;
    private Button mSearchBtn;
    private int mHalfHeight;

    private FrameLayout mEditorLayout;
    private CustomAnimator mAnimator;

    private Button mArrivalBtn;
    private Button mDepatureBtn;
    private EditText mDepatureEt;
    private EditText mArrivalEt;

    private int mCurrentSelectedViewId;

    private InputMethodManager mInputManager;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        final View view = inflater
                .inflate(R.layout.fragment_search, container, false);

        mAnimator = new CustomAnimator(getActivity());
        mInputManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);

        view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right,
                    int bottom, int oldLeft, int oldTop, int oldRight,
                    int oldBottom) {
                v.removeOnLayoutChangeListener(this);
                mHalfHeight = view.getHeight() / 2;

                mEditorLayout.setTranslationY(mHalfHeight);
                mEditorLayout.setAlpha(0f);

                mAnimator.setEditModeHalfHeight(mHalfHeight);
            }
        });

        initView(view);

        getFragmentManager().beginTransaction().add(R.id.edit_mode_fragment_container, new OtherFragment()).commit();

        return view;
    }

    private void initView(View view) {
        mOutwardTv = (TextView) view.findViewById(R.id.outward);
        mOutwardTv.setOnClickListener(this);
        mInwardTv = (TextView) view.findViewById(R.id.inward);
        mInwardTv.setOnClickListener(this);

        mMainContainer = (FrameLayout) view.findViewById(R.id.main_container);
        mStationsContainer = (LinearLayout) view
                .findViewById(R.id.stations_container);
        mDateTimeContainer = (LinearLayout) view
                .findViewById(R.id.date_time_container);
        mFirstSpacer = (FrameLayout) view.findViewById(R.id.first_spacer);
        mSencondSpacer = (FrameLayout) view.findViewById(R.id.second_spacer);

        mPassengersContainer = (LinearLayout) view
                .findViewById(R.id.passengers_container);
        mThirdSpacer = (FrameLayout) view.findViewById(R.id.third_spacer);
        mSearchBtn = (Button) view.findViewById(R.id.btn_search);

        mEditorLayout = (FrameLayout) view.findViewById(R.id.edit_mode_container);

        //From
        mDepatureBtn = (Button) view.findViewById(R.id.btn_departure);
        mDepatureBtn.setOnClickListener(this);
        mDepatureEt = (EditText) view.findViewById(R.id.depature_et);
        //To
        mArrivalBtn = (Button) view.findViewById(R.id.btn_arrival);
        mArrivalBtn.setOnClickListener(this);
        mArrivalEt = (EditText) view.findViewById(R.id.arrival_et);
    }

    @Override
    public void onClick(View v) {

        if (v == mDepatureBtn) {
            focusOn(mDepatureBtn, mStationsContainer, true);
            focusOn(mDepatureBtn, mDateTimeContainer, true);
            focusOn(mDepatureBtn, mSencondSpacer, true);
            stickTo(mFirstSpacer, mDepatureBtn, true);
            fadeOutToBottom(mPassengersContainer, true);
            fadeOutToBottom(mSearchBtn, true);
            fadeOutToBottom(mThirdSpacer, true);
            slideInToTop(mEditorLayout, true);
            mEditorLayout.setVisibility(View.VISIBLE);

            mDepatureBtn.setVisibility(View.GONE);
            mDepatureEt.setClickable(true);
            mDepatureEt.setFocusable(true);
            mDepatureEt.setFocusableInTouchMode(true);
            mDepatureEt.requestFocus();
            mInputManager.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);

        } else if (v == mArrivalBtn) {

            focusOn(mArrivalBtn, mStationsContainer, true);
            focusOn(mArrivalBtn, mDateTimeContainer, true);
            focusOn(mDepatureBtn, mSencondSpacer, true);
            stickTo(mFirstSpacer, mArrivalBtn, true);
            fadeOutToBottom(mPassengersContainer, true);
            fadeOutToBottom(mSearchBtn, true);
            fadeOutToBottom(mThirdSpacer, true);
            slideInToTop(mEditorLayout, true);
            mEditorLayout.setVisibility(View.VISIBLE);

            mArrivalBtn.setVisibility(View.GONE);
            mArrivalEt.setClickable(true);
            mArrivalEt.setFocusable(true);
            mArrivalEt.setFocusableInTouchMode(true);
            mArrivalEt.requestFocus();
            mInputManager.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);

        } else if (v == mOutwardTv) {
            focusOn(mOutwardTv, mStationsContainer, true);
            focusOn(mOutwardTv, mDateTimeContainer, true);
            focusOn(mOutwardTv, mFirstSpacer, true);
            stickTo(mSencondSpacer, mOutwardTv, true);
            fadeOutToBottom(mPassengersContainer, true);
            fadeOutToBottom(mSearchBtn, true);
            fadeOutToBottom(mThirdSpacer, true);
            slideInToTop(mEditorLayout, true);
            mEditorLayout.setVisibility(View.VISIBLE);
        } else if(v == mInwardTv) {

            focusOn(mInwardTv, mStationsContainer, true);
            focusOn(mInwardTv, mDateTimeContainer, true);
            focusOn(mInwardTv, mFirstSpacer, true);
            stickTo(mSencondSpacer, mInwardTv, true);
            fadeOutToBottom(mPassengersContainer, true);
            fadeOutToBottom(mSearchBtn, true);
            fadeOutToBottom(mThirdSpacer, true);
            slideInToTop(mEditorLayout, true);
            mEditorLayout.setVisibility(View.VISIBLE);
        }
        mCurrentSelectedViewId = v.getId();
        ((ActionBarActivity)getActivity()).startSupportActionMode(mCallback);
    }

    private final Rect mTmpRect = new Rect();

    private void focusOn(View v, View movableView, boolean animated) {

        v.getDrawingRect(mTmpRect);
        mMainContainer.offsetDescendantRectToMyCoords(v, mTmpRect);

        movableView.animate().
                translationY(-mTmpRect.top).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setInterpolator(ANIMATION_INTERPOLATOR).
                setListener(new LayerEnablingAnimatorListener(movableView)).
                start();
    }

    private void unfocus(View v, View movableView, boolean animated) {
        movableView.animate().
                translationY(0).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setInterpolator(ANIMATION_INTERPOLATOR).
                setListener(new LayerEnablingAnimatorListener(movableView)).
                start();
    }

    private void fadeOutToBottom(View v, boolean animated) {
        v.animate().
                translationYBy(mHalfHeight).
                alpha(0).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setInterpolator(ANIMATION_INTERPOLATOR).
                setListener(new LayerEnablingAnimatorListener(v)).
                start();
    }

    private void fadeInToTop(View v, boolean animated) {
        v.animate().
                translationYBy(-mHalfHeight).
                alpha(1).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setInterpolator(ANIMATION_INTERPOLATOR).
                setListener(new LayerEnablingAnimatorListener(v)).
                start();
    }

    private void slideInToTop(View v, boolean animated) {
        v.animate().
                translationY(0).
                alpha(1).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setListener(new LayerEnablingAnimatorListener(v)).
                setInterpolator(ANIMATION_INTERPOLATOR);
    }

    private void slideOutToBottom(View v, boolean animated) {
        v.animate().
                translationY(mHalfHeight * 2).
                alpha(0).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setListener(new LayerEnablingAnimatorListener(v)).
                setInterpolator(ANIMATION_INTERPOLATOR);
    }

    private void stickTo(View v, View viewToStickTo, boolean animated) {
        v.getDrawingRect(mTmpRect);
        mMainContainer.offsetDescendantRectToMyCoords(v, mTmpRect);

        v.animate().
                translationY(viewToStickTo.getHeight() - mTmpRect.top).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setInterpolator(ANIMATION_INTERPOLATOR).
                start();
    }

    private void unstickFrom(View v, View viewToStickTo, boolean animated) {
        v.animate().
                translationY(0).
                setDuration(animated ? ANIMATION_DURATION : 0).
                setInterpolator(ANIMATION_INTERPOLATOR).
                setListener(new LayerEnablingAnimatorListener(viewToStickTo)).
                start();
    }

    private ActionMode.Callback mCallback = new ActionMode.Callback() {

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {

            mode.setTitle("From");

            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {

            slideOutToBottom(mEditorLayout, true);

            switch (mCurrentSelectedViewId) {
            case R.id.btn_departure:
                unstickFrom(mFirstSpacer, mDepatureBtn, true);
                fadeInToTop(mPassengersContainer, true);
                fadeInToTop(mSearchBtn, true);
                fadeInToTop(mThirdSpacer, true);
                unfocus(mDepatureBtn, mStationsContainer, true);
                unfocus(mDepatureBtn, mDateTimeContainer, true);
                unfocus(mDepatureBtn, mSencondSpacer, true);

                mDepatureEt.setClickable(false);
                mDepatureEt.setFocusable(false);
                mDepatureBtn.setVisibility(View.VISIBLE);
                mInputManager.hideSoftInputFromWindow(mDepatureEt.getWindowToken(), 0);
                break;
            case R.id.btn_arrival:
                unstickFrom(mFirstSpacer, mArrivalBtn, true);
                fadeInToTop(mPassengersContainer, true);
                fadeInToTop(mSearchBtn, true);
                fadeInToTop(mThirdSpacer, true);
                unfocus(mArrivalBtn, mStationsContainer, true);
                unfocus(mArrivalBtn, mDateTimeContainer, true);
                unfocus(mArrivalBtn, mSencondSpacer, true);

                mArrivalEt.setClickable(false);
                mArrivalEt.setFocusable(false);
                mArrivalBtn.setVisibility(View.VISIBLE);
                mInputManager.hideSoftInputFromWindow(mArrivalEt.getWindowToken(), 0);
                break;
            case R.id.outward:

                unstickFrom(mSencondSpacer, mOutwardTv, true);
                fadeInToTop(mPassengersContainer, true);
                fadeInToTop(mSearchBtn, true);
                fadeInToTop(mThirdSpacer, true);
                unfocus(mOutwardTv, mStationsContainer, true);
                unfocus(mOutwardTv, mDateTimeContainer, true);
                unfocus(mOutwardTv, mFirstSpacer, true);

                break;
            case R.id.inward:

                unstickFrom(mSencondSpacer, mInwardTv, true);
                fadeInToTop(mPassengersContainer, true);
                fadeInToTop(mSearchBtn, true);
                fadeInToTop(mThirdSpacer, true);
                unfocus(mInwardTv, mStationsContainer, true);
                unfocus(mInwardTv, mDateTimeContainer, true);
                unfocus(mInwardTv, mFirstSpacer, true);

                break;
            }
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
//			mode.getMenuInflater().inflate(R.menu.main, menu);
            //此处要返回true
            return true;
//			return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {

            return false;
        }
    };
}

fragment_search.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ScrollView
        android:id="@+id/normal_mode_container"
        style="@style/Form"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        android:paddingBottom="0.0dip"
        android:paddingTop="0.0dip" >

        <RelativeLayout
            android:id="@+id/form_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clipToPadding="false"
            android:orientation="vertical"
            android:paddingBottom="@dimen/spacing_large" >

            <LinearLayout
                android:id="@+id/stations_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/spacing_large"
                android:background="@color/ct_white"
                android:orientation="vertical" >

                <View
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/divider_section_top"
                    android:layout_gravity="top"
                    android:background="@drawable/divider_section_top" />

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" >

                    <ImageView
                        android:id="@+id/departure_icon"
                        android:layout_width="wrap_content"
                        android:layout_height="@dimen/form_field_height"
                        android:layout_alignParentLeft="true"
                        android:layout_alignParentTop="true"
                        android:paddingLeft="@dimen/spacing_large"
                        android:scaleType="center"
                        android:src="@drawable/ic_search_from" />

                    <ImageView
                        android:id="@+id/arrival_icon"
                        android:layout_width="wrap_content"
                        android:layout_height="@dimen/form_field_height"
                        android:layout_alignParentLeft="true"
                        android:layout_below="@id/departure_icon"
                        android:layout_marginTop="@dimen/divider_medium"
                        android:paddingLeft="@dimen/spacing_large"
                        android:scaleType="center"
                        android:src="@drawable/ic_search_to" />

                    <FrameLayout
                        android:id="@+id/departure"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/form_field_height" >

                        <LinearLayout
                            android:id="@+id/departure_search_view"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:orientation="horizontal" >

                            <EditText
                                android:id="@+id/depature_et"
                                android:layout_width="match_parent"
                                android:layout_height="match_parent"
                                android:background="@null"
                                android:clickable="false"
                                android:focusable="false"
                                android:hint="@string/ui_android_search_departure"
                                android:layout_marginLeft="50dip" />
                        </LinearLayout>

                        <Button
                            android:id="@+id/btn_departure"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:background="@null" />
                    </FrameLayout>

                    <View
                        android:id="@+id/stations_container_divider"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/divider_medium"
                        android:layout_below="@id/departure"
                        android:layout_marginLeft="@dimen/spacing_large"
                        android:layout_marginRight="@dimen/spacing_large"
                        android:background="@drawable/divider_medium" />

                    <FrameLayout
                        android:id="@+id/arrival"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/form_field_height"
                        android:layout_below="@id/stations_container_divider" >

                        <LinearLayout
                            android:id="@+id/arrival_search_view"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:orientation="horizontal" >

                            <EditText
                                android:id="@+id/arrival_et"
                                android:layout_width="match_parent"
                                android:layout_height="match_parent"
                                android:layout_marginLeft="50dip"
                                android:background="@null"
                                android:clickable="false"
                                android:focusable="false"
                                android:hint="@string/ui_android_search_arrival" />
                        </LinearLayout>

                        <Button
                            android:id="@+id/btn_arrival"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:background="@null" />
                    </FrameLayout>

                    <ImageButton
                        android:id="@+id/btn_swap_stations"
                        android:layout_width="@dimen/grid_size_medium"
                        android:layout_height="@dimen/grid_size_medium"
                        android:layout_alignParentRight="true"
                        android:layout_centerVertical="true"
                        android:background="@null"
                        android:scaleType="center"
                        android:src="@drawable/ic_search_swap_stations"
                        android:visibility="gone" />
                </RelativeLayout>
            </LinearLayout>

            <FrameLayout
                android:id="@+id/first_spacer"
                android:layout_width="match_parent"
                android:layout_height="@dimen/form_field_height"
                android:layout_below="@id/stations_container"
                android:background="@color/app_background" >

                <View
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/divider_section_bottom"
                    android:layout_gravity="top"
                    android:background="@drawable/divider_section_bottom" />
            </FrameLayout>

            <LinearLayout
                android:id="@+id/date_time_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/stations_container"
                android:layout_marginTop="@dimen/spacing_large"
                android:background="@color/ct_white"
                android:orientation="vertical" >

                <View
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/divider_section_top"
                    android:layout_gravity="top"
                    android:background="@drawable/divider_section_top" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:divider="@drawable/divider_medium"
                    android:dividerPadding="@dimen/spacing_large"
                    android:orientation="vertical"
                    android:showDividers="middle" >

                    <TextView
                        android:id="@+id/outward"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/form_field_height"
                        android:drawableLeft="@drawable/ic_search_outward"
                        android:drawablePadding="@dimen/spacing_medium"
                        android:ellipsize="end"
                        android:gravity="center_vertical"
                        android:hint="@string/ui_android_search_departureDate"
                        android:paddingLeft="@dimen/spacing_large"
                        android:paddingRight="@dimen/spacing_large"
                        android:singleLine="true"
                        android:textSize="@dimen/font_size_medium" />

                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/form_field_height"
                        android:orientation="horizontal" >

                        <TextView
                            android:id="@+id/inward"
                            android:layout_width="0.0dip"
                            android:layout_height="match_parent"
                            android:layout_weight="1.0"
                            android:drawableLeft="@drawable/ic_search_inward"
                            android:drawablePadding="@dimen/spacing_medium"
                            android:ellipsize="end"
                            android:gravity="center_vertical"
                            android:hint="@string/ui_android_search_oneWay"
                            android:paddingLeft="@dimen/spacing_large"
                            android:paddingRight="@dimen/spacing_large"
                            android:singleLine="true"
                            android:textSize="@dimen/font_size_medium" />

                        <ImageButton
                            android:id="@+id/inward_clear_button"
                            android:layout_width="48.0dip"
                            android:layout_height="match_parent"
                            android:background="@drawable/_selector_light"
                            android:src="@drawable/ic_search_view_clear"
                            android:visibility="gone" />
                    </LinearLayout>
                </LinearLayout>
            </LinearLayout>

            <FrameLayout
                android:id="@+id/second_spacer"
                android:layout_width="match_parent"
                android:layout_height="@dimen/form_field_height"
                android:layout_below="@id/date_time_container"
                android:background="@color/app_background" >

                <View
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/divider_section_bottom"
                    android:layout_gravity="top"
                    android:background="@drawable/divider_section_bottom" />
            </FrameLayout>

            <LinearLayout
                android:id="@+id/passengers_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/date_time_container"
                android:layout_marginTop="@dimen/spacing_large"
                android:background="@color/ct_white"
                android:orientation="vertical" >

                <View
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/divider_section_top"
                    android:layout_gravity="top"
                    android:background="@drawable/divider_section_top" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/form_field_height"
                    android:orientation="horizontal" >

                    <TextView
                        android:id="@+id/passengers"
                        android:layout_width="0.0dip"
                        android:layout_height="match_parent"
                        android:layout_weight="1.0"
                        android:drawableLeft="@drawable/ic_search_passengers"
                        android:drawablePadding="@dimen/spacing_medium"
                        android:ellipsize="end"
                        android:gravity="center_vertical"
                        android:hint="@string/ui_android_search_noPassengersSelected"
                        android:paddingLeft="@dimen/spacing_large"
                        android:paddingRight="@dimen/spacing_small"
                        android:singleLine="true"
                        android:textSize="@dimen/font_size_medium" />

                    <ImageButton
                        android:id="@+id/passengers_clear_button"
                        android:layout_width="48.0dip"
                        android:layout_height="match_parent"
                        android:background="@drawable/_selector_light"
                        android:src="@drawable/ic_search_view_clear"
                        android:visibility="gone" />
                </LinearLayout>
            </LinearLayout>

            <FrameLayout
                android:id="@+id/third_spacer"
                android:layout_width="match_parent"
                android:layout_height="@dimen/form_field_height"
                android:layout_below="@id/passengers_container"
                android:background="@color/app_background" >

                <View
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/divider_section_bottom"
                    android:layout_gravity="top"
                    android:background="@drawable/divider_section_bottom" />
            </FrameLayout>

            <Button
                android:id="@+id/btn_search"
                style="@style/Button.Action.Green"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@+id/passengers_container"
                android:layout_marginLeft="@dimen/inset_horizontal_action_button"
                android:layout_marginRight="@dimen/inset_horizontal_action_button"
                android:layout_marginTop="@dimen/spacing_large"
                android:background="@drawable/btn_green"
                android:text="@string/ui_android_search_searchAction" />
        </RelativeLayout>
    </ScrollView>

    <FrameLayout
        android:id="@+id/edit_mode_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="@dimen/form_field_height"
        android:visibility="invisible" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clickable="true"
            android:orientation="vertical"
            android:paddingTop="@dimen/spacing_large" >

            <View
                android:layout_width="match_parent"
                android:layout_height="@dimen/divider_section_top"
                android:layout_gravity="top"
                android:layout_marginLeft="@dimen/inset_horizontal_content"
                android:layout_marginRight="@dimen/inset_horizontal_content"
                android:background="@drawable/divider_section_top" />

            <FrameLayout
                android:id="@+id/edit_mode_fragment_container"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </LinearLayout>
    </FrameLayout>

</FrameLayout>

以上就是我实现的过程。最后以Cyrial的话做个总结:

With the introduction of the new property-based animation framework and Fragments in Android 3.0, the framework provides developers with all the necessary tools to create wonderful and meaningful UIs while still keeping a maintainable and modularized code. Animating Fragments is generally a single ViewPropertyHolder API call away and may drastically improve the way users understand your application. Designing an application is not only about creating a nice static design. It is also about moving graphical elements in a way it is meaningful to users. Transitions both give life to an application and enrich user experience.

《Custom Animations With Fragments》上有6条评论

  1. 有款应用 叫做Morning Routine ,大部分都是5.0以后Material Design 的效果,可以看下 ,非常好

评论已关闭。