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

Detalhamento da implementação do jogo de quebra-cabeça de beleza no Android

Vamos ver o efeito primeiro:

A imagem é cortada em muitas partes, clique para trocar e montar uma imagem completa; assim, o nível também é fácil de projetar3 3;4 4;5 5;6 6;e assim por diante

Adicionamos uma animação de troca, o efeito ainda é bom, na verdade, o jogo é personalizar um controle, vamos começar a jornada de personalização.

Design do jogo

Primeiro vamos analisar como projetar esse jogo:

1、Precisamos de um contêiner que possa conter esses blocos de imagem, para facilitar, estamos prontos para usar RelativeLayout com addRule

2、Cada bloco de imagem, estamos prontos para usar ImageView

3、Clique para trocar, estamos prontos para usar a TranslationAnimation tradicional

Com o design inicial, parece que esse jogo é muito fácil~

Implementação do layout do jogo

Primeiro, vamos preparar para cortar uma imagem em n*n partes, colocadas em posições específicas; tudo o que precisamos fazer é definir o número n, e então, com base na largura ou altura do layout, dividir o menor valor por n e subtrair algumas margens para obter a largura e altura do ImageView~~

Método construtor
/** 
  * Definir o número de itens n*n;padrão é3 
  */ 
 private int mColumn = 3; 
 /** 
  * Largura do layout 
  */ 
 private int mWidth; 
 /** 
  * Padding do layout 
  */ 
 private int mPadding; 
 /** 
  * Armazenar todos os itens 
  */ 
 private ImageView[] mGamePintuItems; 
 /** 
  * The width of the item 
  */ 
 private int mItemWidth; 
 /** 
  * The horizontal and vertical spacing of the item 
  */ 
 private int mMargin = 3; 
 /** 
  * The image of the puzzle 
  */ 
 private Bitmap mBitmap; 
 /** 
  * Store the image bean after cutting 
  */ 
 private List<ImagePiece> mItemBitmaps; 
 private boolean once; 
 public GamePintuLayout(Context context) { 
  this(context, null); 
 } 
 public GamePintuLayout(Context context, AttributeSet attrs) { 
  this(context, attrs, 0); 
 } 
 /**
  * The constructor is used to initialize
  * @param context the context
  * @param attrs the attributes
  * @param defStyle the default style
  * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12
  */
 public GamePintuLayout(Context context, AttributeSet attrs, int defStyle) { 
  super(context, attrs, defStyle); 
 //Convert the set margin value to dp
  mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 
    mMargin, getResources().getDisplayMetrics()); 
  // Set the inner padding of Layout, all sides are consistent, set to the minimum value of the four inner paddings 
  mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(), 
    getPaddingBottom()); 
 }

In the constructor, we convert the set margin value to dp; obtain the layout padding value; as a whole, it is a square, so we take the minimum value of the four directions of padding; as for the margin, as the horizontal and vertical spacing between items, you can extract it as a custom attribute if you like~~

onMeasure
/**
  * used to set the width and height of a custom View
  * @param widthMeasureSpec the width measure spec
  * @param heightMeasureSpec o spec de medição de altura
  * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12
  */
@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
  // Obter o comprimento da borda do layout do jogo 
  mWidth = Math.min(getMeasuredHeight(), getMeasuredWidth()); 
  if (!once) { 
   initBitmap(); 
   initItem(); 
  } 
  once = true; 
  setMeasuredDimension(mWidth, mWidth); 
 }

O onMeasure basicamente é obter a largura do layout, preparar a imagem e inicializar nosso Item, configurar a largura e altura do Item

initBitmap naturalmente é preparar a imagem:

/**
 * Inicializar bitmap
 * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12
 */
private void initBitmap() { 
  if (mBitmap == null) 
   mBitmap = BitmapFactory.decodeResource(getResources(), 
     R.drawable.aa); 
  mItemBitmaps = ImageSplitter.split(mBitmap, mColumn); 
 //Ordenar a imagem
  Collections.sort(mItemBitmaps, new Comparator<ImagePiece>(){ 
   @Override 
   public int compare(ImagePiece lhs, ImagePiece rhs){ 
   //Usamos random para comparar tamanhos
    return Math.random() > 0.5 ? 1 : -1; 
   } 
  }); 
 }

Se não estiver configurado mBitmap, preparamos uma imagem reserva e chamamos ImageSplitter.split para cortar a imagem em n * n Retornar uma Lista<ImagePiece> . Após cortar, precisamos desordena-la, então chamamos o método sort, quanto ao comparador, usamos random para comparar tamanhos, dessa forma, completamos nossa operação de desordenação, você aprova ou não~~

/**
 * Descrição: Classe de fatiamento de imagem
 * Data:2016/9/11-19:53
 * Blog: www.qiuchengjia.cn
 * Autor: qiu
 */
public class ImageSplitter { 
 /** 
  * Cortar a imagem em , peça *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", "largura da bitmap = " + largura + " , altura = " + 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; 
 } 
}
/**
 * Descrição: bean de imagem
 * Data:2016/9/11-19:54
 * Blog: www.qiuchengjia.cn
 * Autor: qiu
 */
public class ImagePiece 
{ 
 public int index = 0; 
 public Bitmap bitmap = null; 
}

É sempre dito que é um processo de cortar e salvar imagens com base na largura, altura e n~~

A imagem salva e o índice do ImagePiece, aliás, esses dois tipos foram encontrados por acaso na internet~~

A imagem está pronta agora, observe que o Item já foi configurado com largura e altura, ou seja, initItems

/**
 * inicializar cada item
 * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12
 */
private void initItem() { 
  // obter largura do Item 
  int childWidth = (mWidth - mPadding * 2 - mMargin * 
  (mColumn - 1)) / mColumn; 
  mItemWidth = childWidth; 
  mGamePintuItems = new ImageView[mColumn * mColumn]; 
  // colocar o 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); 
   // definir a margem horizontal, não a última coluna 
   if ((i + 1) % mColumn != 0) { 
    lp.rightMargin = mMargin; 
   } 
   // se não for a primeira coluna 
    
    lp.addRule(RelativeLayout.RIGHT_OF,// 
      mGamePintuItems[i - 1].getId()); 
   } 
   // se não for a primeira linha,//definir a margem vertical, não a última linha 
   if ((i + 1) > mColumn) { 
     
    lp.addRule(RelativeLayout.BELOW,// 
      mGamePintuItems[i - mColumn].getId()); 
   } 
   addView(item, lp); 
  } 
 }

Pode ver a calculação da largura do nosso Item: childWidth = (mWidth - mPadding 2 - mMargin (mColumn - 1) ) / mColumn; a largura do contêiner, subtraindo o espaçamento interno e o espaçamento entre os Items, e dividindo pelo número de Items em uma linha, obtemos a largura do Item~~

A seguir, é necessário percorrer e gerar o Item, configurando as Regras com base em suas posições, olhe atentamente os comentários~~

Atenção a dois pontos:

1e configuramos o setOnClickListener para o Item, claro, pois nosso jogo é clicar no Item, né~

2e também configuramos a Tag para o Item: item.setTag(i + "_" + mItemBitmaps.get(i).index);

O tag armazena o index, que é a posição correta; e i, que pode ajudar a encontrar a imagem atual do Item em mItemBitmaps: (mItemBitmaps.get(i).bitmap))

Até aqui, o código da layout do nosso jogo terminou~~~

Então, declaramos no arquivo de layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/
 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"
  5dp" >
 </game.qiu.com.beautygame.GamePintuLayout>
</RelativeLayout>

Lembre-se de configurar essa layout no Activity~~

O efeito atual é:

Efeito de alternância do jogo

Alternância inicial

Lembras de que adicionamos o ouvinte onClick aos itens?~~

Agora precisamos implementar, clique em dois itens, suas imagens podem ser trocadas~

Portanto, precisamos de duas variáveis de membro para armazenar esses dois itens e, em seguida, trocá-los

/**
 * Grava o ImageView do primeiro clique
 */
private ImageView mFirst; 
/**
 * Grava o ImageView do segundo clique
 */
private ImageView mSecond; 
/**
 * Evento de clique
 * @param view a view
 * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12
 */ 
@Override 
public void onClick(View v) { 
 /** 
  * Se os dois cliques forem no mesmo 
  */ 
 if (mFirst == v) { 
  mFirst.setColorFilter(null); 
  mFirst = null; 
  return; 
 } 
 //Clique no primeiro item 
 if (mFirst == null) { 
  mFirst = (ImageView) v; 
  mFirst.setColorFilter(Color.parseColor("#"));55FF0000")); 
 } else//Clicamos no segundo Item 
 { 
  mSecond = (ImageView) v; 
  exchangeView(); 
 } 
}

Clicamos no primeiro, ajustamos o efeito de seleção com setColorFilter, clicamos novamente em outro, então estamos prontos para chamar exchangeView para trocar a imagem, claro que este método ainda não foi escrito, vamos deixá-lo por enquanto~

Se clicarmos duas vezes no mesmo, removemos o efeito de seleção, vamos considerar que nada aconteceu;

A seguir, vamos implementar exchangeView:

/**
 * Trocar as imagens de dois itens 
 * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12
 */
 private void exchangeView() { 
  mFirst.setColorFilter(null); 
  String firstTag = (String) mFirst.getTag(); 
  String secondTag = (String) mSecond.getTag(); 
  //Obtém a posição de índice na lista 
  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; 
 }

Deve lembrar do nosso setTag anterior, esqueceu, vá dar uma olhada, ainda estamos falando sobre atenção~

Através de getTag, obtemos o índice na Lista, então obtemos o bitmap para ajustar a troca, finalmente ajustamos o tag;

Até aqui, o efeito de troca foi concluído, o nosso jogo pode terminar~~

O efeito é assim:

Podemos ver que já podemos jogar, quanto ao por que não usar imagens de paisagem frescas, é porque realmente não conseguimos ver qual parte é qual, ou a visão direta da garota é mais intuitiva~

Claro que todos vão reclamar, uau, a transição de animação? Claro que não são duas trocando de lugar, o que diabos é isso?

Também, para o programa, devemos ter uma aspiração, vamos adicionar o efeito de transição de animação~~

Transição de animação suave

Vamos primeiro falar sobre como adicionar, estou pronto para usar TranslationAnimation, e os top, left dos dois Item também são obtidos do contêiner;

Mas, é importante entender que na verdade, o Item apenas mudou setImage, a posição do Item não mudou;

Neste momento, precisamos de um efeito de movimento de animação, por exemplo, A se move para B, não há problema, após a conclusão do movimento, o Item deve retornar, mas a imagem não muda, ainda precisamos ajustar manualmente setImage;

Assim, foi criado um fenômeno, o efeito de transição de animação foi obtido, mas ainda assim haverá um brilho, causado pela transição de imagens;

Para evitar esse fenômeno, para realizar perfeitamente o efeito de transição, aqui introduzimos uma camada de animação, dedicada a efeitos de animação, um pouco semelhante às camadas do PS, vamos ver como fazemos abaixo;

/** 
 * Sinalizador de execução da animação 
 */ 
private boolean isAniming; 
/** 
 * Camada de animação 
 */ 
private RelativeLayout mAnimLayout; 
/**
 * Trocar as imagens de dois itens
 * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12
 */
private void exchangeView(){ 
  mFirst.setColorFilter(null); 
  setUpAnimLayout(); 
  // Adicionar 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); 
  // Adicionar 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); 
  // Definir animação 
  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); 
  // adicionar ouvinte de animação 
  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; 
   } 
  }); 
 } 
 /** 
  * Create animation layer 
  */ 
 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]); 
 }

At the beginning of the exchange, we create an animation layer, then add two identical Items to this layer, hide the original Item, and then freely perform the animation switch, setFillAfter to true~

After the animation is complete, we have quietly exchanged the image of the Item and directly displayed it. This perfectly switches:

The general process:

    1A, B are hidden

    2A's copy animation moves to B's position; B's copy moves to A's position

    3A sets the image to B, removes the copy of B, and A is displayed, thus perfectly fitting together, making the user feel that B is moving over

    4Same as B

Now our effect:

Now that the effect is satisfactory, to prevent users from clicking repeatedly, add a line in the onClick:

@Override 
 public void onClick(View v) 
 { 
  // If the animation is being executed, it should be blocked 
  if (isAniming) 
   return;

By now, our animation switch has been perfectly completed~~

When switching, should we judge whether it has succeeded~~

Judgment of game victory

We have completed the switch, and now we perform the checkSuccess(); judgment; fortunately, we have stored the correct order of the images in the tag~~

/**
 * To determine if the game is successful
 * @author qiu Blog: www.qiuchengjia.cn Time: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(); 
  } 
 } 
 /** 
  * Obter o índice real da imagem 
  * @param tag 
  * @return 
  */ 
 private int getIndexByTag(String tag){ 
  String[] split = tag.split("_"); 
  return Integer.parseInt(split[1]); 
 }

É muito simples, percorrer todos os Item, obter o índice real e a ordem natural, se forem completamente idênticos, significa vitória ~ Vitória entra no próximo nível

Quanto ao código do próximo nível:

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

Resumo

Bem, aí está praticamente o conteúdo do nosso artigo. Quem estiver interessado pode começar a operar por conta própria, o que ajudará muito a entender e aprender. Se tiverem dúvidas, podem deixar um comentário para trocar.

Você também pode gostar