效果图先搞一张让大家看看:
灵感来源:https://github.com/zhangke3016/FlipCards
一:介绍
1.首先介绍:
这个自定义的ViewGroup。灵感来源两个,一个是上面Github开源3D动画效果,一个就是在点击项目的退出登陆按钮时,为什么非要弹出一个对话框?其实用一种动画效果,平滑切换至另一个界面,来代替弹出框。(当然可能退出登陆并不是一个恰当的用例,看个人对app设计的理解了)
2.效果如上:
点击退出登陆按钮,3D切换至 包含退出和取消两个按钮的 界面,点击取消-返回,点击退出-提示正在退出登陆。
支持横向纵向3D切换,自动轮播切换。
3.使用:
先看下XML布局文件:
<com.dup.library.FlipCardViewGroup
android:id="@+id/group"
android:layout_width="250dp"
android:layout_height="100dp"
android:gravity="center"
android:padding="20dp"
app:first_item_index="0">
<Button
android:id="@+id/btn_first"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/btn_bg1"
android:text="退出登录"
android:textColor="#ffffff" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/btn_second"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/btn_bg2"
android:text="退出"
android:textColor="#ffffff" />
<Button
android:id="@+id/btn_third"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/btn_bg3"
android:text="取消"
android:textColor="#ffffff" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/text_bg3"
android:gravity="center"
android:text="退出登录中。。。"
android:textColor="#fff" />
</com.dup.library.FlipCardViewGroup>
最外层是自定义的ViewGroup,可以看到效果图中有三个子界面,那么xml中把三个子界面作为ViewGroup的子View就可以了。
二.实现思路
1.自定义ViewGroup,为了可以使用Gravity属性,这里继承RelativeLayout。
2.关键点:如何互相切换?咱们先不考虑3D切换效果。就是单纯控制其中子View的显示与不显示。那好了,在onLayout()控制其子View的布局位置大小(下面会详细介绍)。
3.点击其中一个子View中的button,点击事件中调用ViewGroup中方法,来控制是哪一个子View显示。
三.关键代码
1.重写ViewGroup中onLayout()和onMeasure():
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
if (count == 0) {
return;
}
for (int i = 0; i < count; ++i) {
View child = getChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
if (i == currentIndex) {
child.layout(0 + getPaddingLeft(), 0 + getPaddingTop(), r - l - getPaddingRight(), b - t - getPaddingBottom());
} else {
child.layout(0, 0, 0, 0);
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; ++i) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
onLayout()方法中:currentIndex就是指的当前要显示的子view的索引。要显示的子view,就使其layout充满父控件(注意padding也要算上)。不显示的子view就使其layout(0,0,0,0)。
2.切换至某一子view公开方法:
/**
* 切换至 第index个item
*
* @param index 要切换至的item index
*/
public void changeToItem(final int index) {
post(new Runnable() {
@Override
public void run() {
int correctIndex = getCorrectIndex(index);
if (correctIndex == currentIndex) {
return;
}
currentIndex = correctIndex;
final FlipCardAnimation animation = new FlipCardAnimation(0, 180, getWidth(), getHeight(), rotateType);
animation.setDuration(duration);
animation.setInterpolator(context, interpolator);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
((FlipCardAnimation) animation).setCanContentChange();
}
});
animation.setOnContentChangeListener(new FlipCardAnimation.OnContentChangeListener() {
@Override
public void contentChange() {
requestLayout();
}
});
startAnimation(animation);
}
});
}
首先改变currentIndex值。这里用到了一个开源的3D动画,当旋转至垂直于屏幕(看不到控件时)会回调OnContentChangeListener,回调时我们requestLayout()请求ViewGroup重新执行一遍onLayout()方法。这样就实现了切换效果了。
3.自动切换:
此ViewGroup也是支持自动切换的,下面是开启自动切换方法:
/**
* 启动自动转动动画
*
* @param fromIndex :开始index
* @param toIndex :结束index
* @param repeatCount:重复次数
* @param startdelay:开始动画延时
* @param idle:间隔动画延时
* @param isReverse:是否反向播放 eg:如果有三个item<br>
* <li>1-1-正向:item播放顺序是:1,2,0,1</li>
* <li>1-1-反向:1,0,2,1</li>
* <li>0-1-正向:0,1</li>
* <li>0-1-反向:0,2,1</li>
* <li>2-1-正向:2,0,1</li>
* <li>2-1-反向:2,1</li>
*/
public void playAnimation(final int fromIndex, final int toIndex, final int repeatCount, final long startdelay, final long idle, final boolean isReverse) {
play_startIndex = fromIndex;
play_endIndex = toIndex;
play_repeatCount = repeatCount;
play_startDelay = (int) startdelay;
play_idle = (int) idle;
play_isreverse = isReverse;
post(new Runnable() {
@Override
public void run() {
//1.瞬間到指定开始index
currentIndex = fromIndex;
requestLayout();
mThread = new PlayThread(fromIndex, toIndex, repeatCount, isReverse);
if (executor != null) {
executor.shutdownNow();
}
executor = new ScheduledThreadPoolExecutor(1);
executor.scheduleWithFixedDelay(mThread, startdelay, idle, TimeUnit.MILLISECONDS);
}
});
}
这里用到了ScheduledThreadPoolExecutor线程池,用来间断性地启动
切换线程(PlayThread)。这个线程会计算出下一个显示的子View的Index,并调用上面介绍过的切换至某一子View的方法。
4.切换线程的run()方法
@Override
public void run() {
super.run();
//如果达到重复次数,或不为infinite就停止
if (hasRepeateCount >= repeateCount && repeateCount != -1) {
executor.shutdownNow();
return;
}
//正向进行
if (!isReverse) {
currentIndex += 1;
if (currentIndex == getChildCount()) {
currentIndex = 0;
}
if (currentIndex == toIndex) {
hasRepeateCount++;
}
changeToItem(currentIndex);
}
//反向进行
else {
currentIndex -= 1;
if (currentIndex == -1) {
currentIndex = getChildCount() - 1;
}
if (currentIndex == toIndex) {
hasRepeateCount++;
}
changeToItem(currentIndex);
}
}
就是一系列的获取下一个Index计算。
5.记得有动画的自定义View要及时停止动画
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
clearAnimation();
if (executor != null) {
executor.shutdownNow();
}
}
6.数据持久化也要做
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putInt(BUNDLE_FIRST, firstIndex);
bundle.putInt(BUNDLE_CURRENT, currentIndex);
bundle.putParcelable(BUNDLE_DEF, super.onSaveInstanceState());
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
currentIndex = bundle.getInt(BUNDLE_CURRENT);
firstIndex = bundle.getInt(BUNDLE_FIRST);
super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_DEF));
} else {
super.onRestoreInstanceState(state);
}
}
这里关键点就是保存数据分两部分,一部分是View自己的数据,一部分就是咱们自己定义的数据。
四.总结
其中自定义的3D动画还是挺有意思的,当旋转过了90度是需要特殊处理的,否则view会倒过来显示。。这个自定义动画在博文最开始有附Github地址,大家可以去看看。我使用此动画时也做了修改,新增了横向转动的效果。具体代码和使用放到了本人Github上面:
传送门:此项目Github地址链接