0x0000 寫在前面
- 在寫這個(gè)游戲之前,只是模糊地記得文曲星上的21點(diǎn)游戲規(guī)則
想當(dāng)然地認(rèn)為就是一個(gè)先后發(fā)牌然后開牌比大小的游戲
結(jié)果百度一下,發(fā)現(xiàn)玩法比我想的復(fù)雜不少
這個(gè)demo里刪掉了分牌的功能够委,并且只支持單人游戲 - 轉(zhuǎn)載請注明
作者和平北路
原文點(diǎn)擊鏈接
0x0001 功能簡介
- 莊家發(fā)牌
- 玩家發(fā)牌
- 玩家選擇發(fā)牌/停牌/放棄
- 比較大小
0x0002 工程結(jié)構(gòu)
- Card是最基本的卡牌對象,包含花色(Suit)和大小(Rank)兩個(gè)屬性
- Deck是去掉了大小王的一副牌
- CardImage是對一張牌的封裝,包含了卡牌的內(nèi)容(Card)和與之綁定的一個(gè)視圖對象(ImageView)测砂,用于在屏幕上進(jìn)行旋轉(zhuǎn)、移動等動作
- BitmapUtils是一個(gè)工具類百匆,用于從一張包含52張卡牌圖案的圖片上裁剪對應(yīng)Card的圖片
- ScreenUtils是一個(gè)工具了砌些,用于獲取屏幕寬高(只適用于全屏)
- Rotate3dAnimation是3D旋轉(zhuǎn)動畫,請自行百度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);
}
}
其實(shí)源碼沒有什么技術(shù)難點(diǎn):
- 所有的動作都以Message方式傳遞給Handler處理加匈,Handler分發(fā)事件調(diào)用各個(gè)方法
- 每發(fā)一張牌存璃,都是在屏幕上new了一個(gè)ImageView,并對這個(gè)ImageView進(jìn)行動畫操作
- 自認(rèn)為寫的比較挫的是對連續(xù)動畫實(shí)現(xiàn)的不好雕拼,現(xiàn)在的硬編碼low爆了纵东。一個(gè)AnimationListener嵌套另一個(gè)AnimationListener,而且還需要添加Callback監(jiān)聽旋轉(zhuǎn)和移動動畫完成后的下一個(gè)操作啥寇,更好的實(shí)現(xiàn)方式是單起Thread篮迎,利用sleep或者wait/notify來實(shí)現(xiàn)動畫的銜接
- Animator的使用沒有想的簡單,說是會改變View屬性示姿,但連續(xù)使用“translation”操作,View對象的邊界其實(shí)沒有改變逊笆,后續(xù)的傳值需要考慮之前的translation賦值栈戳,或者在每次動畫之后調(diào)用View.layout方法更新一遍邊界
0x0004 寫碼感想
- 像Handler、Animator這些東西自以為源碼看了幾遍應(yīng)該手到擒來的难裆,在使用的時(shí)候還是會發(fā)現(xiàn)各種效果實(shí)現(xiàn)和自己想的不一樣
- 行勝于言