前言:最近项目要做一个类似游戏翻取宝箱的功能来代替以前的签到打卡的功能,一开始完全没有思路,就连3D的翻转动画都不知道怎么实现,更别说还要结合一些特别的UI交互,更是无从下手;两天按我的思路实现之后,写到最后逻辑越来越复杂进行不下去,后来在小组组长的指点下,对牌进行了抽象简化了不少逻辑,进我实现最终完成了,^0^,先看下效果哈
1.积分类型(简单的积分上漂)
2.入职红包(从当前位置移动到屏幕右下角我的模块)
3.实物类型(弹出对话框,分享后才能领取)
注:关于翻牌子的奖励规则:一般分为真随机和伪随机。1:真随机就是后台控制概率,完全随机,当然贵重物品的概率都是很低的或者直接是0,只是用来展示的,你永远抽不到的。。;2:伪随机就是不是完全的随机了,比如你的中奖概率更你的等级呀,你的充值金额呀相关联,比如把你的充值金额当做一个计算因数包含在计算你的中奖概率上,这样你充值消费的越多,中奖概率越大。当然这些概率之类的控制都是服务器端控制的,我们做Android前端只是负责调用接口,结合设计UI做展示而已,大头还是在服务器
下面是具体的实现过程步骤(为了用户体验:我们的这个抽奖页面是在用于调用抽奖接口成功之后才会出现的,其实就是只要这个抽奖的Activity能弹出,你的此次抽奖结果就已经确定了,剩下的就是我们客户端实现了,只要用户不管点击哪一个,我们就把你此次抽中的结果放到哪个位置,就是客户端根据调用接口返回来的数据来重新构造数据刷新Adapter)
(一)对不同类型牌进行抽象,把公用的属性和方法放在父类,让每种牌自身都具有执行不同动画的功能,提供不同的方法:(比如根据当前牌是否是抽奖结果决定是否延时翻转,根据当前牌的类型决定牌全部翻 转过来之后执行不同的动画)
(二)3D动画的实现
(三)整体布局是用GridView,设置用户未点击时GridView的默认显示效果
(四)然后是每个item的点击事件,根据抽奖结果,重新构造数据,刷新Adapter,同时根据抽中奖品的类型,来执行不同的UI交互
具体实现(由于是公司项目,项目较大源代码不变给出,只是做部分截图)
(一)抽象牌:
1.牌的类型:空牌(未点击时的默认牌);现金牌;蛙币牌;入职红包牌;实物牌;谢谢参与牌;目录结构如下
2.抽象牌的父类(Card):由于不同类型的牌要展示的效果不一样,而且点击过后翻转过来的View并不是一张简单的图片;比如说入职红包牌,其中的88元,88大礼包,还有背景图片都是不同的控件,因为这些字段都是从服务器获取我们用控件设置上去的,并不是只有一张图片的url让我们去加载,那样的话就简单很多了。这样的话,我们就不能只写一个type的布局了,应为那样的话,我们按正常的做法就是根据服务器给我们对象的类型来显示隐藏或显示不同的布局(可是这里我们有6中类型了呀,以后可能还要加),那样逻辑代码肯定会把你写晕的,而且bug百出,当然这也是对多类型数据对象展示最low的一种方法了;我们当然可以重新适配器中getTypeCount的方法在getView的时候更具不同的type来inflater不同的layout文件,这样做法显然比只用一中type要好,(ps:楼主开始也是这么做的,可是写到中间发现也是比较复杂),但是这样写不便于扩展,后面要加牌的类型我们还是要得改adapter里面getTypeCount和getView里面的代码,不便于扩展,而且每种牌要根据类型 要执行不同的后续操作,考虑再三我们还是得将其抽象成一个Card父类,并在父类中定义一些共有的方法,比如说getView方法返回翻转过来要展示的视图。
代码如下:
public abstract class Card {
Context mContext;
LotteryItem mItem;
View mView;
boolean isHit;//是否是选中的那个
View mFlopView;
VLImageView mFlopView1;
View mFlopView2;
int mIndex = 0;
int mRotateX;
int mRotateY;
public Card(Context context) {
mContext = context;
// getView();
}
public Card(Context context, LotteryItem item) {
mContext = context;
mItem = item;
// getView();
}
public void setHit(boolean b) {
isHit = b;
}
public abstract View getView();
public abstract void prepareAnimation();
public abstract void startAnimation();
public abstract void endAnimation();
public abstract void endAnimation(int left, int itemWidth, int top, int itemHeight);
/* public View getViewDiaplay(){
if (mView!=null){
return mView;
}else{
return null;
}
}*/
public abstract void setRotateXY(int x, int y);
}
3.具体每种类型牌的具体类型(这里只拿其中2种来说明,一种默认图(未翻转类型),一种较为复杂的入职红包类型)。
EmptyCard:(这里只要实现getView方法即可,其余可以不实现,因为这里只是EmptyCard只是未点击时的默认展示牌,且图片也是本地的)
public class EmptyCard extends Card {
public EmptyCard(Context context) {
super(context);
}
@Override
public void endAnimation() {<span style="color:#ff6666;"> </span>
}
@Override
public View getView() {<span style="white-space:pre"> </span><span style="color:#ff0000;">//空牌是一张默认图片,直接加载本地图片,这里是用fresco封装的加载默认图片方法</span>
I90ImageLoaderModel mI90ImageLoaderModel = VLApplication.instance().getModel(I90ImageLoaderModel.class);
View view = View.inflate(mContext, R.layout.item_empty_card, null);
VLImageView cardPic = (VLImageView) view.findViewById(R.id.emptyCardPic);
int rail= VLUtils.dip2px(4);
cardPic.setCornersRadii(rail,rail,rail,rail);
cardPic.setVlScaleType(VLImageView.VLSCALE_TYPE_CENTER_CROP);
cardPic.apply();
mI90ImageLoaderModel.renderDrawableImage(R.drawable.card_default,200,200,cardPic);
mView=view;
return view;
}
@Override
public void prepareAnimation() {
if (isHit){
startAnimation();
}else{ <span style="color:#ff0000;"> // VLScheduler是项目中封装的类似Handler类</span>
VLScheduler.instance.schedule(1000, VLScheduler.THREAD_MAIN, new VLBlock() {
@Override
protected void process(boolean canceled) {
startAnimation();
}
});
}
}
@Override
public void startAnimation() {<span style="color:#ff0000;">//结合Rotate3dAnimation实现3D翻转效果</span>
Rotate3dAnimation rotation = new Rotate3dAnimation(
0, 90, mRotateX, mRotateY, 0.0f, true);
rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new AccelerateInterpolator());
// rotation.setAnimationListener(new);
rotation.setAnimationListener(new Animation.AnimationListener(){
@Override
public void onAnimationEnd(Animation animation) {
mView.post(new SwapViews());
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
}});
mFlopView.startAnimation(rotation);
}
private class SwapViews implements Runnable
{
@Override
public void run() {
mFlopView1.setVisibility(View.GONE);
mFlopView2.setVisibility(View.GONE);
mIndex++;
if (0 == mIndex % 2) {
mFlopView = mFlopView1;
} else {
mFlopView = mFlopView2;
}
mFlopView.setVisibility(View.VISIBLE);
mFlopView.requestFocus();
Rotate3dAnimation rotation = new Rotate3dAnimation(-90, 0, mRotateX, mRotateY, 0.0f, false);
rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new DecelerateInterpolator());
mFlopView.startAnimation(rotation);
}
}
public void endAnimation(int left,int itemWidth,int top,int itemHeight) {
AnimationSet floatAnimSet = new AnimationSet(true);
TranslateAnimation transAnim = new TranslateAnimation(Animation.RELATIVE_TO_SELF,0, Animation.ABSOLUTE, VLUtils.getScreenWidth(mContext
)-left-itemWidth,
Animation.RELATIVE_TO_SELF,0, Animation.ABSOLUTE, VLUtils.getScreenHeight(mContext)-top-itemHeight);
transAnim.setDuration(2000);
ScaleAnimation scaleAnim = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnim.setDuration(2000);
// scaleAnim.setFillAfter(true);
AlphaAnimation alphaAnim = new AlphaAnimation(1.0f, 0.0f);
alphaAnim.setDuration(2000);
floatAnimSet.addAnimation(scaleAnim);
floatAnimSet.addAnimation(transAnim);
floatAnimSet.addAnimation(alphaAnim);
mView.startAnimation(floatAnimSet);
}
@Override
public void setRotateXY(int x, int y) {
mRotateX=x;
mRotateY=y;
}
}
public class JobCard extends Card {
public JobCard(Context context,LotteryItem item) {<span style="color:#ff0000;">//LotteryItem是服务器获取的奖品对象</span>
super(context,item);
}
public void endAnimation(int left,int itemWidth,int top,int itemHeight) {<span style="color:#ff0000;">//入职红包牌对应的是要从所点击的地方执行动画到右下角,其中的参数是从外部调用的地方经过动态测量后传递进来的,另外特别的是在gridView直接执行子View的移动,子View是不会超出GridView这个大的父布局的,而且是从gridView的底部移动的,(分割线的底部,要有多丑就有多丑。。。),所以我们要根据点击的位置重新创建一个牌对象放在整个Activity的根布局上并且它的位置要和我们点击的位置要完全重合,这样才不会漏出破绽;</span>
AnimationSet floatAnimSet = new AnimationSet(true);
TranslateAnimation transAnim = new TranslateAnimation(Animation.RELATIVE_TO_SELF,0, Animation.ABSOLUTE, VLUtils.getScreenWidth(mContext
)-left-itemWidth,
Animation.RELATIVE_TO_SELF,0, Animation.ABSOLUTE, VLUtils.getScreenHeight(mContext)-top-itemHeight);
transAnim.setDuration(2000);
ScaleAnimation scaleAnim = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnim.setDuration(2000);
// scaleAnim.setFillAfter(true);
AlphaAnimation alphaAnim = new AlphaAnimation(1.0f, 0.0f);
alphaAnim.setDuration(2000);
floatAnimSet.addAnimation(scaleAnim);
floatAnimSet.addAnimation(transAnim);
floatAnimSet.addAnimation(alphaAnim);
floatAnimSet.setFillAfter(true);
mView.startAnimation(floatAnimSet);
}
@Override
public View getView() {<span style="color:#ff0000;">//根据传递进来的LotteryItem属性,初始化View视图</span>
I90ImageLoaderModel mI90ImageLoaderModel = VLApplication.instance().getModel(I90ImageLoaderModel.class);
int rail = VLUtils.dip2px(4);
View view = View.inflate(mContext, R.layout.item_job_card, null);
VLImageView cardEmpty = (VLImageView) view.findViewById(R.id.jobEmpty);
cardEmpty.setCornersRadii(rail,rail,rail,rail);
cardEmpty.setVlScaleType(VLImageView.VLSCALE_TYPE_CENTER_CROP);
cardEmpty.apply();
mI90ImageLoaderModel.renderDrawableImage(R.drawable.card_default,200,200,cardEmpty);
View cardView = view.findViewById(R.id.jobView);
VLImageView cardTypeImage = (VLImageView) view.findViewById(R.id.jobCardTypeImage);
TextView cardCount =(TextView) view.findViewById(R.id.jobCardCount);
TextView cardType = (TextView)view.findViewById(R.id.jobCardType);
ImageView cardBg = (ImageView)view.findViewById(R.id.jobCardBg);
cardTypeImage.setCornersRadii(0, 0, rail, rail);
cardTypeImage.setVlScaleType(VLImageView.VLSCALE_TYPE_CENTER_CROP);
cardTypeImage.apply();
mI90ImageLoaderModel.renderDrawableImage(R.drawable.card_image1, 200, 52, cardTypeImage);
cardCount.setText(VLUtils.androidSizeSpan((int)mItem.getPrototype().getCash()+"", 0xfff55c3d, 0, 60));
cardCount.append(VLUtils.androidSizeSpan("元", 0xfff55c3d, 0, 24));
cardBg.setVisibility(isHit ? View.GONE : View.VISIBLE);
if (TextUtils.isEmpty(mItem.getPrototype().getName())){
cardType.setText("入职红包");
}else{
cardType.setText(mItem.getPrototype().getName());
}
mFlopView=cardEmpty;
mFlopView1=cardEmpty;
mFlopView2=cardView;
mView=view;
return view;
}
@Override
public void prepareAnimation() {
VLDebug.logD("startAnimation=" + " class=" + getClass().getSimpleName() + "isHit="+isHit);
if (isHit){<span style="color:#ff0000;">//根据当前牌是否是用户本次抽中的牌,来决定是否来延迟翻转,从gif图中可以看出,用户点中的牌立刻执行翻转动画,而其他的牌有1秒钟的延迟</span>
startAnimation();
}else{<span style="white-space:pre"> </span> <span style="color:#ff0000;"> //延迟翻转</span>
VLScheduler.instance.schedule(1000, VLScheduler.THREAD_MAIN, new VLBlock() {
@Override
protected void process(boolean canceled) {
startAnimation();
}
});
}
}
@Override
public void startAnimation() {<span style="color:#ff0000;">//真正翻转动画</span>
VLDebug.logD("startAnimation= do" + " class=" + getClass().getSimpleName() + "isHit="+isHit);
Rotate3dAnimation rotation = new Rotate3dAnimation(
0, 90, mRotateX, mRotateY, 0.0f, true);
rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new AccelerateInterpolator());
// rotation.setAnimationListener(new);
rotation.setAnimationListener(new Animation.AnimationListener(){
@Override
public void onAnimationEnd(Animation animation) {
mView.post(new SwapViews());
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
}});
mFlopView.startAnimation(rotation);
}
@Override
public void endAnimation() {
}
<span style="color:#ff0000;">// Rotate3dAnimation结合该类实现3d翻转,原理是2个View同时执行翻转动画,到一定角度显示隐藏一个布局</span>
private class SwapViews implements Runnable
{
@Override
public void run() {
mFlopView1.setVisibility(View.GONE);
mFlopView2.setVisibility(View.GONE);
mIndex++;
if (0 == mIndex % 2) {
mFlopView = mFlopView1;
} else {
mFlopView = mFlopView2;
}
mFlopView.setVisibility(View.VISIBLE);
Rotate3dAnimation rotation = new Rotate3dAnimation(-90, 0, mRotateX, mRotateY, 0.0f, false);
rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new DecelerateInterpolator());
mFlopView.startAnimation(rotation);
}
}
@Override
public void setRotateXY(int x, int y) {<span style="color:#ff0000;">//设置旋转的轴线(属性动画的同学应该都知道执行动画应该按照一定的轴线旋转)这里的值也是外部调用时动态测量每个GridView的Item的宽高后设置进来的</span>
mRotateX=x;
mRotateY=y;
}
}
(二 )3D动画的执行
3D翻转的动画Android本身并不支持,可以利用valueAnimator和objectAnimator中rotationY,rotationX,rotationZ可以实现由3d的效果,但是只支持一张图片,所以用的时候还是要封装,楼主当时做的时候对valueAnimator和objectAnimator还不是很了解,所以还是从网上搜罗了一个自定义动画Rotate3dAnimation,这个应该是一个老外写的ps:注释全是英文。。。后来楼主看了这位大神的blog:
http://my.csdn.net/harvic880925的博客后自己实现了一个支持3d动画的自定义控件(
http://blog.csdn.net/themelove/article/details/50619771)效果还不错
(三)每个item的点击事件(根据抽奖结果,重新构造数据,刷新Adapter,同时根据抽中奖品的类型,来执行不同的UI交互,当然是在Activity和Adapter里进行了)
MyFlopAdaper
public class MyFlopAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private ArrayList<Card> mDatas;
private int mHeight;
VLAsyncHandler mVlAsyncHandler;
public void setLoadEnd(VLAsyncHandler vlAsyncHandler) {
mVlAsyncHandler = vlAsyncHandler;
}
public MyFlopAdapter(Context context, int height) {
mInflater = LayoutInflater.from(context);
mHeight = height;
}
public void setData(ArrayList<Card> datas) {
mDatas = datas;
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.group_daydayflop_row_item, parent, false);
convertView.setLayoutParams(new AbsListView.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, mHeight / 3- VLUtils.dip2px(8)));
}
if (position
== parent.getChildCount()) {
FrameLayout frameLayout = (FrameLayout) convertView;
frameLayout.removeAllViews();
frameLayout.addView(mDatas.get(position).getView(), new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
if (null != mVlAsyncHandler && position == mDatas.size() - 1) {
mVlAsyncHandler.handlerSuccess();
}
}
VLDebug.logD("getView position= " + position);
return convertView;
}
}
DaydayFlopActivity(代码太多,直接上这几个相关类的压缩包吧)