项目地址https://github.com/Yalantis/Euclid
依赖:CircularReveal
ListViewAnimations
理解这个效果一定要看明白xml布局里每个元素直接的关系,以及每个元素在Z轴上的层次关系。
<!-- 灵活的使用相对布局-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 最底层的就是那个Following Toolbar-->
<FrameLayout
android:id="@+id/toolbar_list"
android:layout_width="match_parent"
android:layout_height="@dimen/height_toolbar"
android:background="@color/orange">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-bold"
android:text="@string/title_following"
android:textColor="@color/black"
android:textSize="@dimen/text_size_toolbar_title" />
</FrameLayout>
<!-- 第二层的ListView-->
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar_list"
android:background="@color/gray"
android:divider="@drawable/list_divider"
tools:listitem="@layout/list_item" />
<!-- 以下页面就是简介详情的页面了开始默认是隐藏的-->
<!-- 从上到下的Toolbar-->
<RelativeLayout
android:id="@+id/toolbar_profile"
android:layout_width="match_parent"
android:layout_height="@dimen/height_toolbar"
android:background="@color/gray"
android:clickable="true"
android:orientation="horizontal"
android:visibility="invisible">
<ImageView
android:id="@+id/toolbar_profile_back"
android:layout_width="@dimen/height_toolbar"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:padding="15dp"
android:src="@drawable/ic_arrow_left" />
<RelativeLayout
android:layout_width="200dp"
android:layout_height="33dp"
android:layout_centerInParent="true"
android:background="@drawable/bg_black_oval">
<TextView
style="@style/TextViewProfileToolbarTitle"
android:layout_margin="2dp"
android:background="@drawable/bg_orange_oval"
android:text="@string/toolbar_bio"
android:textColor="@color/black" />
<TextView
style="@style/TextViewProfileToolbarTitle"
android:layout_alignParentRight="true"
android:text="@string/toolbar_details"
android:textColor="@color/white" />
</RelativeLayout>
</RelativeLayout>
<!-- 简介详情-->
<LinearLayout
android:id="@+id/wrapper_profile_details"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/height_profile_picture_with_toolbar"
android:background="@color/white"
android:clickable="true"
android:orientation="vertical"
android:visibility="invisible">
<TextView
android:id="@+id/text_view_profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:fontFamily="sans-serif-light"
android:textColor="@color/gray"
android:textSize="32sp"
tools:text="SOPHIA" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<TextView
android:id="@+id/text_view_profile_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:textColor="@color/gray"
android:textSize="18sp"
tools:text="@string/lorem_ipsum_long" />
</ScrollView>
</LinearLayout>
<!-- 简介里的悬浮按钮,在RelativeLayout最顶层-->
<include
android:id="@+id/button_profile"
layout="@layout/button_round_msg"
android:layout_width="@dimen/size_button_message"
android:layout_height="@dimen/size_button_message"
android:layout_alignParentRight="true"
android:layout_marginRight="15dp"
android:layout_marginTop="@dimen/margin_top_button_message"
android:visibility="invisible" />
</RelativeLayout>
怎么给ListView添加适配器就不说了,但是里面有一个圆形图(其实不是圆形的,只是因为ImagView上面还被一个View覆盖,但是这个View有个特殊的背景)
看看是什么背景
//好像我稍微改了点东西。。。
private ShapeDrawable buildAvatarCircleOverlay() {
//获取半径
int radius = dpToPx(getCircleRadiusDp());
ShapeDrawable overlay = new ShapeDrawable(new RoundRectShape(null,
new RectF(
sScreenWidth / 2 - radius,
sProfileImageHeight / 2 - radius,
sScreenWidth / 2 - radius,
sProfileImageHeight / 2 - radius),
//1、2左上角,3、4右上角,5、6右下角,7、8左下角,如果没弧度的话,传入null即可。
new float[]{radius, radius, radius, radius, radius, radius, radius, radius}));
overlay.getPaint().setColor(getResources().getColor(R.color.gray));
overlay.getPaint().setAntiAlias(true);
return overlay;
}
RoundRectShape里的第二个参数里的值就是与外部矩形的边距。相当于把一个矩形中间个掏空了。掏空后中间就是透明的了,所以你就可以看到一个圆形的图片。
看重点了ListView的点击事件
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//正在执行动画状态
mState = EuclidState.Opening;
showProfileDetails((Map<String, Object>) parent.getItemAtPosition(position), view);
}
});
private void showProfileDetails(Map<String, Object> item, final View view) {
//防止出现滚动
mListView.setEnabled(false);
//根据view的Top算出相对动画时间
int profileDetailsAnimationDelay =
getMaxDelayShowDetailsAnimation() * Math.abs(view.getTop())/ sScreenWidth;
//一个和当前点击Item元素一样布局的view,
// 覆盖在当前item元素上,让你以为是当前item元素进行了移动
addOverlayListItem(item, view);
//执行Reveal动画和把覆盖在Item元素上的布局进行从当前位置移动到toolbar的Bottom位置
startRevealAnimation(profileDetailsAnimationDelay);
//执行简介里的button缩放动画和ToolBar、以及简介内容的位移动画
animateOpenProfileDetails(profileDetailsAnimationDelay);
}
我们一个一个函数的看
addOverlayListItem
private void addOverlayListItem(Map<String, Object> item, View view) {
if (mOverlayListItemView == null) {
mOverlayListItemView =
//跟ListView里的Item元素 的布局是一样的,这很重要!!
getLayoutInflater().inflate(R.layout.overlay_list_item, mWrapper, false);
} else {
mWrapper.removeView(mOverlayListItemView);
}
mOverlayListItemView.findViewById(R.id.view_avatar_overlay).setBackground(sOverlayShape);
Picasso.with(EuclidActivity.this)
.load((Integer) item.get(EuclidListAdapter.KEY_AVATAR))
.resize(sScreenWidth, sProfileImageHeight)
.centerCrop()
.placeholder(R.color.blue)
.into((ImageView) mOverlayListItemView.findViewById(R.id.image_view_reveal_avatar));
// Picasso.with(EuclidActivity.this)
// .load((Integer) item.get(EuclidListAdapter.KEY_AVATAR))
// .resize(sScreenWidth, sProfileImageHeight)
// .centerCrop()
// .placeholder(R.color.blue)
// .into((ImageView) mOverlayListItemView.findViewById(R.id.image_view_avatar));
((TextView) mOverlayListItemView.findViewById(R.id.text_view_name)).setText((String) item.get(EuclidListAdapter.KEY_NAME));
((TextView) mOverlayListItemView.findViewById(R.id.text_view_description)).setText((String) item.get(EuclidListAdapter.KEY_DESCRIPTION_SHORT));
setProfileDetailsInfo(item);
//动态的添加一个View到相对布局里,并且把这个View显示在你点击ListView的那个Item一样的位置。
//这样你就感觉是item发生了动画(其实不是)。
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.topMargin = view.getTop() + mToolbar.getHeight();
params.bottomMargin = -(view.getBottom() - mListView.getHeight());
mWrapper.addView(mOverlayListItemView, params);
mToolbar.bringToFront();
}
startRevealAnimation
private void startRevealAnimation(final int profileDetailsAnimationDelay) {
mOverlayListItemView.post(new Runnable() {
@Override
public void run() {
getAvatarRevealAnimator().start();
getAvatarShowAnimator(profileDetailsAnimationDelay).start();
}
});
}
private SupportAnimator getAvatarRevealAnimator() {
//很简单的Reveal动画
final LinearLayout mWrapperListItemReveal = (LinearLayout) mOverlayListItemView.findViewById(R.id.wrapper_list_item_reveal);
//计算最终半径
int finalRadius = Math.max(mOverlayListItemView.getWidth(), mOverlayListItemView.getHeight());
//重点是SupportAnimator ,如果不懂,看CircularReveal
final SupportAnimator mRevealAnimator = ViewAnimationUtils.createCircularReveal(
mWrapperListItemReveal, //目标View
sScreenWidth / 2, //目标View的发生动画的x轴上的点
sProfileImageHeight / 2, //目标View的发生动画的y轴上的点
dpToPx(getCircleRadiusDp()), //起始半径
finalRadius); //最终的半径
mRevealAnimator.setDuration(getRevealAnimationDuration());
mRevealAnimator.addListener(new SupportAnimator.AnimatorListener() {
@Override
public void onAnimationStart() {
mWrapperListItemReveal.setVisibility(View.VISIBLE);
mOverlayListItemView.setX(0);
}
@Override
public void onAnimationEnd() {
}
@Override
public void onAnimationCancel() {
}
@Override
public void onAnimationRepeat() {
}
});
return mRevealAnimator;
}
private Animator getAvatarShowAnimator(int profileDetailsAnimationDelay) {
//很简单的属性位移动画
//把覆盖的view从当前的位置移动到ToolBar的Bottom位置
final Animator mAvatarShowAnimator = ObjectAnimator.ofFloat(mOverlayListItemView, View.Y, mOverlayListItemView.getTop(), mToolbarProfile.getBottom());
mAvatarShowAnimator.setDuration(profileDetailsAnimationDelay + getAnimationDurationShowProfileDetails());
mAvatarShowAnimator.setInterpolator(new DecelerateInterpolator());
return mAvatarShowAnimator;
}
private void animateOpenProfileDetails(int profileDetailsAnimationDelay) {
createOpenProfileButtonAnimation();
getOpenProfileAnimatorSet(profileDetailsAnimationDelay).start();
}
private void createOpenProfileButtonAnimation() {
//给悬浮按钮的缩放动画,注意,不是立即就执行动画
if (mProfileButtonShowAnimation == null) {
mProfileButtonShowAnimation = AnimationUtils.loadAnimation(this, R.anim.profile_button_scale);
mProfileButtonShowAnimation.setDuration(getAnimationDurationShowProfileButton());
mProfileButtonShowAnimation.setInterpolator(new AccelerateInterpolator());
mProfileButtonShowAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mButtonProfile.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
}
profile_button_scale.xml
<?xml version="1.0" encoding="utf-8"?>
<set android:shareInterpolator="false"
xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:fromXScale="0.0"
android:toXScale="1.0"
android:fromYScale="0.0"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:fillAfter="false"/>
</set>
//包含2个动画,一个是ToolBar的位移向下和详细页位移向上的动画
private AnimatorSet getOpenProfileAnimatorSet(int profileDetailsAnimationDelay) {
if (mOpenProfileAnimatorSet == null) {
List<Animator> profileAnimators = new ArrayList<>();
profileAnimators.add(getOpenProfileToolbarAnimator());
profileAnimators.add(getOpenProfileDetailsAnimator());
mOpenProfileAnimatorSet = new AnimatorSet();
mOpenProfileAnimatorSet.playTogether(profileAnimators);
mOpenProfileAnimatorSet.setDuration(getAnimationDurationShowProfileDetails());
}
mOpenProfileAnimatorSet.setStartDelay(profileDetailsAnimationDelay);
mOpenProfileAnimatorSet.setInterpolator(new DecelerateInterpolator());
return mOpenProfileAnimatorSet;
}
private Animator getOpenProfileToolbarAnimator() {
Animator mOpenProfileToolbarAnimator = ObjectAnimator.ofFloat(mToolbarProfile, View.Y, -mToolbarProfile.getHeight(), 0);
mOpenProfileToolbarAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mToolbarProfile.setX(0);
mToolbarProfile.bringToFront();
mToolbarProfile.setVisibility(View.VISIBLE);
mProfileDetails.setX(0);
mProfileDetails.bringToFront();
mProfileDetails.setVisibility(View.VISIBLE);
mButtonProfile.setX(mInitialProfileButtonX);
mButtonProfile.bringToFront();
}
@Override
public void onAnimationEnd(Animator animation) {
//ToolBar动画一结束就开始悬浮按钮的缩放动画 mButtonProfile.startAnimation(mProfileButtonShowAnimation);
mState = EuclidState.Opened;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
return mOpenProfileToolbarAnimator;
}
//很简单的位移动画,从屏幕底下移动到指定位置
private Animator getOpenProfileDetailsAnimator() {
Animator mOpenProfileDetailsAnimator = ObjectAnimator.ofFloat(mProfileDetails, View.Y,
getResources().getDisplayMetrics().heightPixels,
getResources().getDimensionPixelSize(R.dimen.height_profile_picture_with_toolbar));
return mOpenProfileDetailsAnimator;
}
看看关闭时的动画
// 一系列的X轴位移动画
private AnimatorSet getCloseProfileAnimatorSet() {
if (mCloseProfileAnimatorSet == null) {
Animator profileToolbarAnimator = ObjectAnimator.ofFloat(mToolbarProfile, View.X,
0, mToolbarProfile.getWidth());
Animator profilePhotoAnimator = ObjectAnimator.ofFloat(mOverlayListItemView, View.X,
0, mOverlayListItemView.getWidth());
profilePhotoAnimator.setStartDelay(getStepDelayHideDetailsAnimation());
Animator profileButtonAnimator = ObjectAnimator.ofFloat(mButtonProfile, View.X,
mInitialProfileButtonX, mOverlayListItemView.getWidth() + mInitialProfileButtonX);
profileButtonAnimator.setStartDelay(getStepDelayHideDetailsAnimation() * 2);
Animator profileDetailsAnimator = ObjectAnimator.ofFloat(mProfileDetails, View.X,
0, mToolbarProfile.getWidth());
profileDetailsAnimator.setStartDelay(getStepDelayHideDetailsAnimation() * 2);
List profileAnimators = new ArrayList<>();
profileAnimators.add(profileToolbarAnimator);
profileAnimators.add(profilePhotoAnimator);
profileAnimators.add(profileButtonAnimator);
profileAnimators.add(profileDetailsAnimator);
mCloseProfileAnimatorSet = new AnimatorSet();
mCloseProfileAnimatorSet.playTogether(profileAnimators);
mCloseProfileAnimatorSet.setDuration(getAnimationDurationCloseProfileDetails());
mCloseProfileAnimatorSet.setInterpolator(new AccelerateInterpolator());
mCloseProfileAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
if (mListViewAnimator != null) {
mListViewAnimator.reset();
mListViewAnimationAdapter.notifyDataSetChanged();
}
}
@Override
public void onAnimationEnd(Animator animation) {
mToolbarProfile.setVisibility(View.INVISIBLE);
mButtonProfile.setVisibility(View.INVISIBLE);
mProfileDetails.setVisibility(View.INVISIBLE);
mListView.setEnabled(true);
mListViewAnimator.disableAnimations();
mState = EuclidState.Closed;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
return mCloseProfileAnimatorSet;
}
ListView里的item位移效果是在listviewanimations里写的。
总结:
xml布局很重要。
点击ListView的item时,先将一个相同的View添加到当前的相对布局,
接着执行4个动画:
1、以相同的ListView里item布局覆盖在当前相对布局里。
2、执行Reveal动画,从当前圆形的半径执行到覆盖view的最大半径。
3、覆盖View的位移动画
4、ToolBar向下位移,详细view向上位移。
5、第3补动画一结束对悬浮按钮进行缩放动画
退出动画:
就是执行一系列的x轴的位移动画。各个之间有个先后顺序。