0x0000 写在前面
- 在写这个游戏之前,只是模糊地记得文曲星上的21点游戏规则
想当然地认为就是一个先后发牌然后开牌比大小的游戏
结果百度一下,发现玩法比我想的复杂不少
这个demo里删掉了分牌的功能,并且只支持单人游戏 - 转载请注明
作者和平北路
原文点击链接
0x0001 功能简介
- 庄家发牌
- 玩家发牌
- 玩家选择发牌/停牌/放弃
- 比较大小
0x0002 工程结构
- Card是最基本的卡牌对象,包含花色(Suit)和大小(Rank)两个属性
- Deck是去掉了大小王的一副牌
- CardImage是对一张牌的封装,包含了卡牌的内容(Card)和与之绑定的一个视图对象(ImageView),用于在屏幕上进行旋转、移动等动作
- BitmapUtils是一个工具类,用于从一张包含52张卡牌图案的图片上裁剪对应Card的图片
- ScreenUtils是一个工具了,用于获取屏幕宽高(只适用于全屏)
- Rotate3dAnimation是3D旋转动画,请自行百度Android官方源码
0x0003 源码分析
public class Card {
enum Suit {HEART, SPADE, DIAMOND, CLUB}
enum Rank {ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING}
private Suit suit;
private Rank rank;
public Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
public Suit getSuit() {
return suit;
}
public Rank getRank() {
return rank;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
Card card = (Card) obj;
if (card.suit != suit || card.rank != rank) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = suit.ordinal();
result = 31 * result + rank.ordinal();
return result;
}
}
public class Deck {
Collection<Card.Suit> suits = Arrays.asList(Card.Suit.values());
Collection<Card.Rank> ranks = Arrays.asList(Card.Rank.values());
private List<Card> deck = new ArrayList<>();
public Deck() {
init();
}
private void init() {
for (Iterator<Card.Suit> i = suits.iterator(); i.hasNext(); /*do nothing*/) {
Card.Suit suit = i.next();
for (Iterator<Card.Rank> j = ranks.iterator(); j.hasNext(); /*do nothing*/) {
deck.add(new Card(suit, j.next()));
}
}
}
public List<Card> getDeck() {
return deck;
}
}
public class CardImage {
private static final long DURATION = 500L;
private static final float DEPTH_Z = 0f;
private int translationX;
private int translationY;
private Card card;
private ImageView image;
private AnimatorSet animatorSet;
private IAnimationCallback callback;
public CardImage(Card card, ImageView image) {
this.card = card;
this.image = image;
translationX = 0;
translationY = 0;
}
public void setCallback(IAnimationCallback callback) {
this.callback = callback;
}
public void translate(final int x, final int y) {
if (image == null) {
return;
}
if (image.getLeft() == 0 || image.getTop() == 0) {
final ViewTreeObserver observer = image.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
translate(x, y);
observer.removeOnGlobalLayoutListener(this);
}
});
return;
}
if (null != animatorSet) {
animatorSet.end();
}
image.clearAnimation();
ObjectAnimator animX = ObjectAnimator.ofFloat(image, "translationX", x - image.getLeft());
ObjectAnimator animY = ObjectAnimator.ofFloat(image, "translationY", y - image.getTop());
translationX = x - image.getLeft();
translationY = y - image.getTop();
animatorSet = new AnimatorSet();
animatorSet.playTogether(animX, animY);
animatorSet.setDuration(DURATION);
animatorSet.start();
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (null != callback)
callback.onTranslationEnd();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
public void front() {
if (image == null) {
return;
}
if (null != animatorSet) {
animatorSet.end();
}
image.clearAnimation();
final int x = image.getLayoutParams().width / 2 + translationX;
final int y = image.getLayoutParams().height / 2 + translationY;
Animation firstHalf = rotate(0, 90, x, y);
firstHalf.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
image.setImageBitmap(BitmapUtils.getCardImage(image.getContext(), card));
Animation secondHalf = rotate(270, 360, x, y);
secondHalf.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (null != callback)
callback.onFrontEnd();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
private Animation rotate(float startDegree, float endDegree, float centerX, float centerY) {
Rotate3dAnimation anim = new Rotate3dAnimation(
startDegree, endDegree, centerX, centerY, DEPTH_Z, false);
anim.setDuration(DURATION);
anim.setFillAfter(true);
image.startAnimation(anim);
return anim;
}
public Card getCard() {
return card;
}
public ImageView getImage() {
return image;
}
public interface IAnimationCallback {
void onFrontEnd();
void onTranslationEnd();
}
}
public class BitmapUtils {
public static Bitmap cards;
public static Bitmap getCardImage(Context context, Card card) {
if (null == card)
return null;
if (null == cards)
cards = BitmapFactory.decodeResource(context.getResources(), R.drawable.cards);
int rows = Card.Suit.values().length;
int cols = Card.Rank.values().length;
int width = cards.getWidth() / cols;
int height = cards.getHeight() / rows;
int x = width * card.getRank().ordinal();
int y = height * card.getSuit().ordinal();
return Bitmap.createBitmap(cards, x, y, width, height);
}
}
public class ScreenUtils {
private static int sScreenWidth;
private static int sScreenHeight;
public static void init(Context context) {
Resources resources = context.getResources();
DisplayMetrics dm = resources.getDisplayMetrics();
sScreenWidth = dm.widthPixels;
sScreenHeight = dm.heightPixels;
}
public static int getScreenWidth(Context context) {
if (sScreenWidth <= 0) {
init(context);
}
return sScreenWidth;
}
public static int getScreenHeight(Context context) {
if (sScreenHeight <= 0) {
init(context);
}
return sScreenHeight;
}
}
public class MainActivity extends Activity {
private static final int MSG_PLAYER_DEAL = 0x00;
private static final int MSG_PLAYER_HIT = 0x01;
private static final int MSG_PLAYER_STAND = 0x02;
private static final int MSG_PLAYER_FOLD = 0x03;
private static final int MSG_BANKER_DEAL = 0x04;
private static final int MSG_BANKER_DEAL_HIDE = 0x05;
private static final int MSG_BANKER_FRONT_HIDE = 0x06;
private static final int MSG_RESET = 0x07;
private static final int TEN = 10;
private static final int BLACK_JACK = 21;
private static final int CARD_WIDTH = 225;
private static final int CARD_HEIGHT = 315;
private static final int CARD_MARGIN = 10;
private static final int CARD_MARGIN_TOP = 20;
private static final int CARD_MARGIN_BOTTOM = 500;
private int screenWidth;
private int screenHeight;
private Random random;
private ViewGroup parent;
private View hit;
private View stand;
private View fold;
private List<Card> cards;
private List<CardImage> playerCards;
private List<CardImage> bankerCards;
private Handler msgHandle = new Handler() {
@Override
public void handleMessage(Message msg) {
int what = msg.what;
switch (what) {
case MSG_PLAYER_DEAL:
dealPlayer();
break;
case MSG_BANKER_DEAL:
dealBanker();
break;
case MSG_BANKER_DEAL_HIDE:
dealBankerHide();
break;
case MSG_BANKER_FRONT_HIDE:
frontBankerHide();
break;
case MSG_PLAYER_HIT:
hit();
break;
case MSG_PLAYER_STAND:
stand();
break;
case MSG_PLAYER_FOLD:
fold();
break;
case MSG_RESET:
reset();
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
ScreenUtils.init(getApplicationContext());
setContentView(R.layout.activity_main);
screenWidth = ScreenUtils.getScreenWidth(this);
screenHeight = ScreenUtils.getScreenHeight(this);
random = new Random();
cards = new Deck().getDeck();
playerCards = new ArrayList<>();
bankerCards = new ArrayList<>();
parent = (ViewGroup) findViewById(R.id.activity_main);
hit = findViewById(R.id.hit);
hit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
hit();
}
});
stand = findViewById(R.id.stand);
stand.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stand();
}
});
fold = findViewById(R.id.fold);
fold.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
fold();
}
});
deal();
}
private void deal() {
disableButtons();
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_PLAYER_DEAL), 500);
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_BANKER_DEAL), 2000);
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_PLAYER_DEAL), 3500);
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_BANKER_DEAL_HIDE), 5000);
}
private void dealPlayer() {
disableButtons();
CardImage player = new CardImage(
cards.remove(random.nextInt(cards.size())),
createCardImage());
player.setCallback(new CardImage.IAnimationCallback() {
@Override
public void onFrontEnd() {
updatePlayerCardsLocation();
}
@Override
public void onTranslationEnd() {
if (playerCards.size() >= 2) {
enableButtons();
}
if (playerCards.size() > 2) {
check();
}
}
});
player.front();
playerCards.add(player);
}
private void dealBanker() {
disableButtons();
CardImage banker = new CardImage(
cards.remove(random.nextInt(cards.size())),
createCardImage());
banker.setCallback(new CardImage.IAnimationCallback() {
@Override
public void onFrontEnd() {
updateBankerCardsLocation();
}
@Override
public void onTranslationEnd() {
if (bankerCards.size() > 2) {
disableButtons();
if (!check()) {
if (count(bankerCards) > count(playerCards)) {
lose();
} else {
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_BANKER_DEAL), 2000);
}
}
}
}
});
banker.front();
bankerCards.add(banker);
}
private void dealBankerHide() {
disableButtons();
CardImage banker = new CardImage(
cards.remove(random.nextInt(cards.size())),
createCardImage());
banker.setCallback(new CardImage.IAnimationCallback() {
@Override
public void onFrontEnd() {
}
@Override
public void onTranslationEnd() {
check();
}
});
bankerCards.add(banker);
updateBankerCardsLocation();
}
private void frontBankerHide() {
disableButtons();
CardImage banker = bankerCards.get(1);
banker.front();
}
private ImageView createCardImage() {
ImageView card = new ImageView(this);
card.setImageResource(R.drawable.card_back);
card.setScaleType(ImageView.ScaleType.CENTER_CROP);
RelativeLayout.LayoutParams layoutParams =
new RelativeLayout.LayoutParams(CARD_WIDTH, CARD_HEIGHT);
layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
layoutParams.rightMargin = 40;
parent.addView(card, layoutParams);
return card;
}
private int[] getPlayerCardLocation(int num) {
int centerX = screenWidth / 2;
int size = playerCards.size();
int x = centerX
- ((size - 1) * CARD_MARGIN + size * CARD_WIDTH) / 2
+ num * (CARD_MARGIN + CARD_WIDTH);
int y = screenHeight - CARD_MARGIN_BOTTOM;
return new int[]{x, y};
}
private void updatePlayerCardsLocation() {
int size = playerCards.size();
for (int i = 0; i < size; i++) {
int[] location = getPlayerCardLocation(i);
CardImage cardImage = playerCards.get(i);
cardImage.translate(location[0], location[1]);
}
}
private int[] getBankerCardLocation(int num) {
int centerX = screenWidth / 2;
int size = bankerCards.size();
int x = centerX
- ((size - 1) * CARD_MARGIN + size * CARD_WIDTH) / 2
+ num * (CARD_MARGIN + CARD_WIDTH);
int y = CARD_MARGIN_TOP;
return new int[]{x, y};
}
private void updateBankerCardsLocation() {
int size = bankerCards.size();
for (int i = 0; i < size; i++) {
int[] location = getBankerCardLocation(i);
CardImage cardImage = bankerCards.get(i);
cardImage.translate(location[0], location[1]);
}
}
private boolean check() {
boolean isBankerBlackJack = isBlackJack(bankerCards);
boolean isPlayerBlackJack = isBlackJack(playerCards);
if (isBusted(playerCards) || (isBankerBlackJack && !isPlayerBlackJack)) {
lose();
return true;
} else if (isBusted(bankerCards) || (!isBankerBlackJack && isPlayerBlackJack)) {
win();
return true;
} else if (isBankerBlackJack && isPlayerBlackJack) {
draw();
return true;
}
return false;
}
private boolean isBlackJack(List<CardImage> handCards) {
return count(handCards) == BLACK_JACK;
}
private boolean isBusted(List<CardImage> handCards) {
return count(handCards) > BLACK_JACK;
}
private int count(List<CardImage> handCards) {
int total = 0;
if (null == handCards) {
return total;
}
for (CardImage cardImage : handCards) {
if (cardImage == null || cardImage.getCard() == null) {
continue;
}
Card card = cardImage.getCard();
if (card.getRank() == Card.Rank.ACE) {
if (total + 11 > BLACK_JACK) {
total += 1;
} else {
total += 11;
}
} else if (card.getRank().ordinal() >= 9) {
total += TEN;
} else {
total += (card.getRank().ordinal() + 1);
}
}
return total;
}
private void reset() {
cards = new Deck().getDeck();
for (CardImage cardImage : playerCards) {
if (cardImage != null && cardImage.getImage() != null) {
parent.removeView(cardImage.getImage());
}
}
for (CardImage cardImage : bankerCards) {
if (cardImage != null && cardImage.getImage() != null) {
parent.removeView(cardImage.getImage());
}
}
playerCards.clear();
bankerCards.clear();
for (int i = 0; i < 8; i++) {
msgHandle.removeMessages(i);
}
deal();
}
private void enableButtons() {
hit.setClickable(true);
stand.setClickable(true);
fold.setClickable(true);
}
private void disableButtons() {
hit.setClickable(false);
stand.setClickable(false);
fold.setClickable(false);
}
private void hit() {
disableButtons();
msgHandle.sendMessage(Message.obtain(msgHandle, MSG_PLAYER_DEAL));
}
private void stand() {
disableButtons();
if (count(bankerCards) > count(playerCards)) {
lose();
return;
}
msgHandle.sendMessage(Message.obtain(msgHandle, MSG_BANKER_DEAL));
}
private void fold() {
disableButtons();
frontBankerHide();
lose();
}
private void win() {
disableButtons();
Toast.makeText(MainActivity.this, "you win!", Toast.LENGTH_SHORT).show();
msgHandle.sendMessage(Message.obtain(msgHandle, MSG_BANKER_FRONT_HIDE));
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_RESET), 4000);
}
private void lose() {
disableButtons();
Toast.makeText(MainActivity.this, "you lose!", Toast.LENGTH_SHORT).show();
msgHandle.sendMessage(Message.obtain(msgHandle, MSG_BANKER_FRONT_HIDE));
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_RESET), 4000);
}
private void draw() {
disableButtons();
Toast.makeText(MainActivity.this, "game draws!", Toast.LENGTH_SHORT).show();
msgHandle.sendMessage(Message.obtain(msgHandle, MSG_BANKER_FRONT_HIDE));
msgHandle.sendMessageDelayed(Message.obtain(msgHandle, MSG_RESET), 4000);
}
}
其实源码没有什么技术难点:
- 所有的动作都以Message方式传递给Handler处理,Handler分发事件调用各个方法
- 每发一张牌,都是在屏幕上new了一个ImageView,并对这个ImageView进行动画操作
- 自认为写的比较挫的是对连续动画实现的不好,现在的硬编码low爆了。一个AnimationListener嵌套另一个AnimationListener,而且还需要添加Callback监听旋转和移动动画完成后的下一个操作,更好的实现方式是单起Thread,利用sleep或者wait/notify来实现动画的衔接
- Animator的使用没有想的简单,说是会改变View属性,但连续使用“translation”操作,View对象的边界其实没有改变,后续的传值需要考虑之前的translation赋值,或者在每次动画之后调用View.layout方法更新一遍边界
0x0004 写码感想
- 像Handler、Animator这些东西自以为源码看了几遍应该手到擒来的,在使用的时候还是会发现各种效果实现和自己想的不一样
- 行胜于言