English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Android에서 아름다운 패ズル 게임 구현 설명

먼저 효과를 보겠습니다:

이미지를 많이 잘라서, 클릭하여 교환하여 전체 이미지를 완성합니다. 이렇게 하면 레벨도 쉽게 설계할 수 있습니다3 3을 계속 합니다4 4을 계속 합니다5 5을 계속 합니다6 6을 계속 합니다

교체 애니메이션을 추가해 두었는데, 효과는 꽤 좋습니다. 사실 이 게임은 컨트롤을 직접 정의한 것입니다. 이제 직접 정의하는 여정에 시작해 보겠습니다.

게임 설계

먼저 이 게임을 어떻게 설계할지 분석해 보겠습니다:

1를 넣을 수 있는 컨테이너가 필요합니다. 편리함을 위해RelativeLayout과 addRule를 사용할 계획입니다

2각 이미지의 블록을 ImageView로 사용하려고 합니다

3을 잘라서, n

기본적인 설계를 마친 후, 이 게임은 매우 쉬운 것 같습니다~

게임 레이아웃의 구현

먼저, 우리는 한 장의 이미지를 n*n개의 부분으로图片를 잘라서, 지정된 위치에 배치합니다. 우리는 n 이라는 숫자를 설정하고, 레이아웃의 너비나 높이 중 작은 값을 n으로 나누고, 여러 가지 여유를 뺀다면 ImageView의 너비와 높이를 얻을 수 있습니다~~

생성자
/** 
  * Item의 수 n을 설정합니다*n;기본적으로3 
  */ 
 private int mColumn = 3; 
 /** 
  * 레이아웃의 너비 
  */ 
 private int mWidth; 
 /** 
  * 레이아웃의 패딩 
  */ 
 private int mPadding; 
 /** 
  * 전체 Item을 저장합니다. 
  */ 
 private ImageView[] mGamePintuItems; 
 /** 
  * 아이템의 너비 
  */ 
 private int mItemWidth; 
 /** 
  * 아이템의 가로와 세로 마진 
  */ 
 private int mMargin = 3; 
 /** 
  * 스테이지의 이미지 
  */ 
 private Bitmap mBitmap; 
 /** 
  * 이미지를 잘라낸 후 저장하는 bean 
  */ 
 private List<ImagePiece> mItemBitmaps; 
 private boolean once; 
 public GamePintuLayout(Context context) { 
  this(context, null); 
 } 
 public GamePintuLayout(Context context, AttributeSet attrs) { 
  this(context, attrs, 0); 
 } 
 /**
  * 생성자 함수는 초기화를 위해 사용됩니다.
  * @param context the context
  * @param attrs the attrs
  * @param defStyle the def style
  * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
  */
 public GamePintuLayout(Context context, AttributeSet attrs, int defStyle) { 
  super(context, attrs, defStyle); 
 //설정된 마진 값을 dp로 변환합니다
  mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 
    mMargin, getResources().getDisplayMetrics()); 
  // Layout의 내부 마진을 설정합니다. 네 방향 모두 일관되게 설정되며, 네 마진 중 가장 작은 값을 설정합니다 
  mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(), 
    getPaddingBottom()); 
 }

생성자 메서드에서는 설정된 마진 값을 dp로 변환합니다;레이아웃의 패딩 값을 얻습니다;전체가 정方形이므로 패딩 네 방향 중 가장 작은 값을 선택합니다;마진은 아이템 간의 가로와 세로 간격으로, 원하시면 사용자 정의 속성으로 추출할 수 있습니다~~

onMeasure
/**
  * 사용자 정의 View의 너비와 높이를 설정하는 데 사용됩니다.
  * @param widthMeasureSpec the width measure spec
  * @param heightMeasureSpec 높이 측정 스펙
  * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
  */
@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
  // 게임 레이아웃의 변을 얻습니다 
  mWidth = Math.min(getMeasuredHeight(), getMeasuredWidth()); 
  if (!once) { 
   initBitmap(); 
   initItem(); 
  } 
  once = true; 
  setMeasuredDimension(mWidth, mWidth); 
 }

onMeasure에서는 주로 레이아웃의 너비를 얻고, 이미지를 준비하고, Item을 초기화하며, Item의 너비와 높이를 설정하는 것입니다

initBitmap는 자연스럽게 이미지를 준비하는 것입니다:

/**
 * bitmap 초기화
 * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
 */
private void initBitmap() { 
  if (mBitmap == null) 
   mBitmap = BitmapFactory.decodeResource(getResources(), 
     R.drawable.aa); 
  mItemBitmaps = ImageSplitter.split(mBitmap, mColumn); 
 //이미지를 정렬합니다
  Collections.sort(mItemBitmaps, new Comparator<ImagePiece>(){ 
   @Override 
   public int compare(ImagePiece lhs, ImagePiece rhs){ 
   //random 랜덤 비교 크기를 사용합니다
    return Math.random() > 0.5 ? 1 : -1; 
   } 
  }); 
 }

이곳에 mBitmap이 설정되지 않았다면 대체 이미지를 준비하고 ImageSplitter.split을 호출하여 이미지를 n으로 자릅니다 * n을 반환하는 List<ImagePiece> . 슬라이스가 끝나면 순서를 섞어야 하므로 sort 메서드를 호출하고, 비교기는 random 랜덤 비교 크기를 사용하여, 이렇게 우리는 랜덤 섞기 작업을 완료했습니다, 좋아요?

/**
 * Description: 이미지 슬라이스 클래스
 * 데이터:2016/9/11-19:53
 * 블로그: www.qiuchengjia.cn
 * 저자: qiu
 */
public class ImageSplitter { 
 /** 
  * 이미지를 자르기 , piece *piece 
  * @param bitmap 
  * @param piece 
  * @return 
  */ 
 public static List<ImagePiece> split(Bitmap bitmap, int piece){ 
  List<ImagePiece> pieces = new ArrayList<ImagePiece>(piece * piece); 
  int width = bitmap.getWidth(); 
  int height = bitmap.getHeight(); 
  Log.e("TAG", "bitmap Width = " + width + " , height = " + height); 
  int pieceWidth = Math.min(width, height) / piece; 
  for (int i = 0; i < piece; i++){ 
   for (int j = 0; j < piece; j++){ 
    ImagePiece imagePiece = new ImagePiece(); 
    imagePiece.index = j + i * piece; 
    int xValue = j * pieceWidth; 
    int yValue = i * pieceWidth; 
    imagePiece.bitmap = Bitmap.createBitmap(bitmap, xValue, yValue, 
      pieceWidth, pieceWidth); 
    pieces.add(imagePiece); 
   } 
  } 
  return pieces; 
 } 
}
/**
 * 설명: 이미지 베인
 * 데이터:2016/9/11-19:54
 * 블로그: www.qiuchengjia.cn
 * 저자: qiu
 */
public class ImagePiece 
{ 
 public int index = 0; 
 public Bitmap bitmap = null; 
}

다시 말해 너비와 높이, n에 따라 이미지를 잘라 저장하는 과정입니다~~

ImagePiece에 저장된 이미지와 인덱스, 이 두 개의 클래스는 나의 실수로 인터넷에서 발견했습니다~~

이미지는 이제 준비되었습니다. 지금 Item의 생성이 너비와 높이를 설정했으므로 initItems

/**
 * 각각의 item을 초기화합니다
 * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
 */
private void initItem() { 
  // Item의 너비를 얻기 
  int childWidth = (mWidth - mPadding * 2 - mMargin * 
  (mColumn - 1)) / mColumn; 
  mItemWidth = childWidth; 
  mGamePintuItems = new ImageView[mColumn * mColumn]; 
  // Item을 배치합니다 
  for (int i = 0; i < mGamePintuItems.length; i++) { 
   ImageView item = new ImageView(getContext()); 
   item.setOnClickListener(this); 
   item.setImageBitmap(mItemBitmaps.get(i).bitmap); 
   mGamePintuItems[i] = item; 
   item.setId(i + 1); 
    + "_" + mItemBitmaps.get(i).index); 
   
    new LayoutParams(mItemWidth, 
     mItemWidth); 
   // 수평 간격을 설정합니다, 마지막 열이 아닌 경우 
   if ((i + 1) % mColumn != 0) { 
    lp.rightMargin = mMargin; 
   } 
   // 첫 번째 열이 아니라면 
    
    lp.addRule(RelativeLayout.RIGHT_OF,// 
      mGamePintuItems[i - 1].getId()); 
   } 
   // 첫 번째 행이 아니라면,//수직 간격을 설정합니다, 마지막 행이 아닌 경우 
   if ((i + 1) > mColumn) { 
     
    lp.addRule(RelativeLayout.BELOW,// 
      mGamePintuItems[i - mColumn].getId()); 
   } 
   addView(item, lp); 
  } 
 }

아래는 우리의 Item 너비 계산을 볼 수 있습니다: childWidth = (mWidth - mPadding 2 - mMargin (mColumn - 1) ) / mColumn; 컨테이너의 너비는 자신의 내간격을 제거하고, Item 간의 간격을 제거한 후, Item 한 행의 개수로 나누면 Item의 너비를 얻습니다~~

다음은 Item을 순회하며 생성하는 것입니다. 그들의 위치에 Rule을 설정하며, 주의 깊게 주석을 보세요~~

이 점을 주의하세요:

1、우리는 Item에 setOnClickListener을 설정했습니다. 당연히요, 우리의 게임은 Item을 클릭하는 게 아닌가요?

2、우리는 Item에 Tag를 설정했습니다: item.setTag(i + "_" + mItemBitmaps.get(i).index);

tag에 index가 저장되어 있으며, 이는 올바른 위치이며, 또한 i가 현재 아이템의 이미지를 mItemBitmaps에서 찾아내는 데 도움이 됩니다: (mItemBitmaps.get(i).bitmap))

이제, 우리 게임 레이아웃 코드는 끝났습니다~~~

그런 다음 레이아웃 파일에서 다음과 같이 선언합니다:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent" >
 <game.qiu.com.beautygame.GamePintuLayout
  android:id="@"+id/id_gameview"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:layout_centerInParent="true"
  android:padding="5dp" >
 </game.qiu.com.beautygame.GamePintuLayout>
</RelativeLayout>

Activity에서 이 레이아웃을 설정해야 합니다~~

지금의 효과는:

게임 전환 효과

초기 전환

아이템에 onClick 리스너를 추가했던 기억나요~~

지금 두 아이템을 클릭하면 그들의 이미지가 교환되도록 구현해야 합니다~

그래서, 두 개의 멤버 변수를 두 아이템을 저장하고 나서 교환하려고 합니다

/**
 * 첫 번째 클릭을 기록하는 ImageView
 */
private ImageView mFirst; 
/**
 * 두 번째 클릭을 기록하는 ImageView
 */
private ImageView mSecond; 
/**
 * 클릭 이벤트
 * @param view the view
 * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
 */ 
@Override 
public void onClick(View v) { 
 /** 
  * 두 번째 클릭이 같은 아이템이면 
  */ 
 if (mFirst == v) { 
  mFirst.setColorFilter(null); 
  mFirst = null; 
  return; 
 } 
 //첫 번째 아이템을 클릭했습니다 
 if (mFirst == null) { 
   
  mFirst.setColorFilter(Color.parseColor("#, ")),55FF0000")); 
 } else//두 번째 Item을 클릭합니다; 
 { 
  mSecond = (ImageView) v; 
  exchangeView(); 
 } 
}

첫 번째를 클릭하면 setColorFilter를 사용하여 선택 효과를 설정합니다. 다시 클릭하면 다른 것을 클릭하면 exchangeView를 호출하여 이미지를 교환할 준비를 합니다. 물론 이 메서드는 아직 작성되지 않았기 때문에 일단 두고 있습니다~

두 번 연속으로 같은 것을 클릭하면 선택 효과를 제거합니다. 그리고 무엇이 일어났는지 무시합니다;

다음으로, exchangeView를 구현해 보겠습니다:

/**
 * 두 Item 이미지를 교환 
 * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
 */
 private void exchangeView() { 
  mFirst.setColorFilter(null); 
  String firstTag = (String) mFirst.getTag(); 
  String secondTag = (String) mSecond.getTag(); 
  //List에서의 인덱스 위치를 가져옵니다; 
  String[] firstImageIndex = firstTag.split("_"); 
  String[] secondImageIndex = secondTag.split("_"); 
  mFirst.setImageBitmap(mItemBitmaps.get(Integer 
    .parseInt(secondImageIndex[0])).bitmap); 
  mSecond.setImageBitmap(mItemBitmaps.get(Integer 
    .parseInt(firstImageIndex[0])).bitmap); 
  mFirst.setTag(secondTag); 
  mSecond.setTag(firstTag); 
  mFirst = mSecond = null; 
 }

아직 기억나실까요? 이전의 setTag를요. 잊었다면, 다시 보러 가세요. 그리고 우리는 주의를 기울였습니다~

getTag를 통해 List에서의 인덱스를 가져오고, bitmap을 교환 설정하고, 마지막으로 tag를 교환합니다;

이제 우리의 교환 효과는 끝났습니다. 우리의 게임이 끝났습니다~~

효과는 이렇습니다:

이제 우리는 이미 가능합니다. 깨끗한 풍경 사진을 사용하지 않은 이유는, 그 부분이 어디에 해당하는지 알 수 없기 때문입니다. 여자 친구가 직관적으로 보여서 그렇습니다;

물론, 많은 사람들이 토로할 것입니다. 그저 애니메이션 전환을 왜 안 쓰냐고요? 명확히 말하면, 두 개의 이미지가 서로의 위치를 교환하는 것처럼 보이지 않는 거죠, 이게 무엇인가요?

물론이죠, 프로그램에 대해 우리는 추구해야 합니다. 아래에서는 애니메이션 전환 효과를 추가해 보겠습니다~~

无缝的动画切换

먼저 추가하는 방법에 대해 이야기해 보겠습니다. 저는 TranslationAnimation을 사용할 계획입니다. 그리고 두 개의 Item의 top과 left는 레이어에서 가져옵니다;

그러나 이해해야 합니다. 실제로는 Item이 setImage이 변했을 뿐, Item의 위치는 변하지 않았습니다;

지금은 애니메이션 이동 효과가 필요합니다. 예를 들어, A가 B로 이동하는 것처럼, 문제가 없습니다. 이동이 완료되면, Item이 돌아가야 합니다. 그러나 이미지는 변하지 않았습니다. 여전히 setImage을 수동으로 해야 합니다;

이렇게 되어 애니메이션 전환 효과가 생겼지만, 마지막에는 빛나는 현상이 생기고, 이는 이미지를 전환한 것으로 인해 발생합니다;

이러한 현상을 피하기 위해 완벽한 전환 효과를 달성하기 위해, 여기서는 애니메이션 레이어를 도입했습니다. 애니메이션 효과를 위해 특별히 만들어진 레이어입니다. PS의 레이어와 비슷합니다. 아래에서 어떻게 하게 될지 보겠습니다;

/** 
 * 애니메이션 실행 여부 표시자 
 */ 
private boolean isAniming; 
/** 
 * 애니메이션 레이아웃 
 */ 
private RelativeLayout mAnimLayout; 
/**
 * 두 Item 이미지를 교환
 * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
 */
private void exchangeView(){ 
  mFirst.setColorFilter(null); 
  setUpAnimLayout(); 
  // FirstView 추가 
  ImageView first = new ImageView(getContext()); 
  first.setImageBitmap(mItemBitmaps 
    .get(getImageIndexByTag((String) mFirst.getTag())).bitmap); 
  LayoutParams lp = new LayoutParams(mItemWidth, mItemWidth); 
  lp.leftMargin = mFirst.getLeft() - mPadding; 
  lp.topMargin = mFirst.getTop() - mPadding; 
  first.setLayoutParams(lp); 
  mAnimLayout.addView(first); 
  // SecondView 추가 
  ImageView second = new ImageView(getContext()); 
  second.setImageBitmap(mItemBitmaps 
    .get(getImageIndexByTag((String) mSecond.getTag())).bitmap); 
  LayoutParams lp2 = new LayoutParams(mItemWidth, mItemWidth); 
  lp2.leftMargin = mSecond.getLeft() - mPadding; 
  lp2.topMargin = mSecond.getTop() - mPadding; 
  second.setLayoutParams(lp2); 
  mAnimLayout.addView(second); 
  // 애니메이션 설정 
  TranslateAnimation anim = new TranslateAnimation(0, mSecond.getLeft() 
    - mFirst.getLeft(), 0, mSecond.getTop() - mFirst.getTop()); 
  anim.setDuration(300); 
  anim.setFillAfter(true); 
  first.startAnimation(anim); 
  TranslateAnimation animSecond = new TranslateAnimation(0, 
    mFirst.getLeft() - mSecond.getLeft(), 0, mFirst.getTop() 
      - mSecond.getTop()); 
  animSecond.setDuration(300); 
  animSecond.setFillAfter(true); 
  second.startAnimation(animSecond); 
  // 추가 애니메이션 리스너 
  anim.setAnimationListener(new AnimationListener(){ 
   @Override 
   public void onAnimationStart(Animation animation){ 
    isAniming = true; 
    mFirst.setVisibility(INVISIBLE); 
    mSecond.setVisibility(INVISIBLE); 
   } 
   @Override 
   public void onAnimationRepeat(Animation animation){ 
   } 
   @Override 
   public void onAnimationEnd(Animation animation){ 
    String firstTag = (String) mFirst.getTag(); 
    String secondTag = (String) mSecond.getTag(); 
    String[] firstParams = firstTag.split("_"); 
    String[] secondParams = secondTag.split("_"); 
    mFirst.setImageBitmap(mItemBitmaps.get(Integer 
      .parseInt(secondParams[0])).bitmap); 
    mSecond.setImageBitmap(mItemBitmaps.get(Integer 
      .parseInt(firstParams[0])).bitmap); 
    mFirst.setTag(secondTag); 
    mSecond.setTag(firstTag); 
    mFirst.setVisibility(VISIBLE); 
    mSecond.setVisibility(VISIBLE); 
    mFirst = mSecond = null; 
    mAnimLayout.removeAllViews(); 
        //checkSuccess(); 
    isAniming = false; 
   } 
  }); 
 } 
 /** 
  * 애니메이션 레이어 생성 
  */ 
 private void setUpAnimLayout(){ 
  if (mAnimLayout == null){ 
   mAnimLayout = new RelativeLayout(getContext()); 
   addView(mAnimLayout); 
  } 
 } 
 private int getImageIndexByTag(String tag){ 
  String[] split = tag.split("_"); 
  return Integer.parseInt(split[0]); 
 }

교환을 시작할 때, 우리는 애니메이션 레이어를 생성하고, 그 레이어에 두 개의 완전히 동일한 Item을 추가하고, 원래의 Item을 숨기고, 애니메이션切换를 자유롭게 수행하고, setFillAfter를 true로 설정합니다~

애니메이션이 끝나면, 우리는 조용히 Item의 이미지를 교환하고, 그렇게 바로 표시하여 완벽하게切换됩니다:

대략적인 과정:

    1、A, B 숨기기

    2、A 복제 애니메이션을 B 위치로 이동시키고; B 복제를 A 위치로 이동시키기

    3、A 이미지를 B로 설정하고, B 복제를 제거하고, A를 표시하여 완벽하게切换되고, 사용자는 B가 이동한 것처럼 느낍니다

    4、B와 동일

지금의 효과:

지금 효과가 만족스러우면~~ 사용자가 잔뜩 클릭하지 않도록 onClick에 한 문장을 추가합니다:

@Override 
 public void onClick(View v) 
 { 
  // 애니메이션을 실행 중이면 블록 
  if (isAniming) 
   return;

이제 우리의 애니메이션切换는 완벽하게 끝났습니다~~

이동할 때, 우리가 성공했는지�断해야 하나~~

게임 승리�断정

이미지의 올바른顺序을 tag에 저장했기 때문에, 이미지를切换할 때 checkSuccess();를 호출합니다; 운이 좋게~~

/**
 * 게임이 성공했는지�断하기 위해
 * @author qiu 블로그: www.qiuchengjia.cn 시간:2016-09-12
 */
private void checkSuccess(){ 
  boolean isSuccess = true; 
  for (int i = 0; i < mGamePintuItems.length; i++){ 
   ImageView first = mGamePintuItems[i]; 
   Log.e("TAG", getIndexByTag((String) first.getTag()) + ""); 
   if (getIndexByTag((String) first.getTag()) != i){ 
    isSuccess = false; 
   } 
  } 
  if (isSuccess){ 
   Toast.makeText(getContext(), "Success , Level Up !", 
     Toast.LENGTH_LONG).show(); 
   // nextLevel(); 
  } 
 } 
 /** 
  * 获得图片的真正索引 
  * @param tag 
  * @return 
  */ 
 private int getIndexByTag(String tag){ 
  String[] split = tag.split("_"); 
  return Integer.parseInt(split[1 
 }

매우 간단합니다. 모든 Item을 순회하며 Tag를 통해 실제 인덱스와 순서를 비교하여 완전히 일치하면 승리~~ 승리 후 다음 단계로 이동합니다

다음 단계의 코드에 대해서는:

public void nextLevel(){ 
  this.removeAllViews(); 
  mAnimLayout = null; 
  mColumn++; 
  initBitmap(); 
  initItem(); 
 }

결론

좋아요, 이제 우리가 이 글에서 설명한 내용은 기본적으로 끝났습니다. 관심이 있는 분들은 스스로 실습해 보세요. 이렇게 하면 학습을 이해하는 데 더 많은 도움이 됩니다. 의문이 있으면 댓글을 달고 교류하세요.

좋아하는 것