English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
let's take a look at the effect first:
the picture is divided into many pieces, click to swap and form a complete picture; this also makes the level design very easy3 3;4 4;5 5;6 6; go on and on
added a switch animation, the effect is still quite good, in fact, the game is just a custom control, let's start our custom journey below.
game design
Firstly, let's analyze how to design this game:
1, we need a container that can hold these picture blocks, for convenience, we are ready to use RelativeLayout with addRule to implement
2, each picture block, we are ready to use ImageView
3, click to swap, we are ready to use the traditional TranslationAnimation to implement
With the preliminary design, it feels like this game is so easy~
game layout implementation
Firstly, we are ready to implement the ability to cut a picture into n*n pieces, placed at specified positions; we just need to set the number n, and then based on the smaller value of the layout width or height, divide it by n, and subtract some margin to get the width and height of our ImageView~~
constructor /** * sets the number of items n*n; default is3 */ private int mColumn = 3; /** * layout width */ private int mWidth; /** * layout padding */ private int mPadding; /** * stores all the items */ private ImageView[] mGamePintuItems; /** * The width of the item */ private int mItemWidth; /** * The horizontal and vertical margins of the item */ private int mMargin = 3; /** * The image for 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 internal padding of Layout, all sides are consistent, set to the minimum value of the four internal paddings mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); }
In the constructor, we convert the set margin value to dp; obtain the layout padding value; since the whole is a square, we take the minimum value of the four directions of padding; as for 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 the height measure spec * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Get the edge length of the game layout mWidth = Math.min(getMeasuredHeight(), getMeasuredWidth()); if (!once) { initBitmap(); initItem(); } once = true; setMeasuredDimension(mWidth, mWidth); }
In onMeasure, it mainly gets the layout width, then prepares the image, and initializes our Item, setting the width and height for the Item
initBitmap naturally means preparing the image:
/** * Initialize 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); //Sort the images Collections.sort(mItemBitmaps, new Comparator<ImagePiece>(){ @Override public int compare(ImagePiece lhs, ImagePiece rhs){ //We use random to compare the size randomly return Math.random() > 0.5 ? 1 : -1; } }); }
If we have not set mBitmap, we prepare a backup image and then call ImageSplitter.split to cut the image into n * n Returns a List<ImagePiece>. After slicing, we need to shuffle the order, so we call the sort method, and for the comparator, we use random to compare the size randomly, so we have completed our shuffle operation, praise or not~~
/** * Description: Image slicing class * Data:2016/9/11-19:53 * Blog: www.qiuchengjia.cn * Author: qiu */ public class ImageSplitter { /** * Cut the image into pieces, 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; } }
/** * Description: Image bean * Data:2016/9/11-19:54 * Blog: www.qiuchengjia.cn * Author: qiu */ public class ImagePiece { public int index = 0; public Bitmap bitmap = null; }
It is always about the process of cutting and saving images according to width, height, and n~~
The images saved by ImagePiece and the index, by the way, these two classes were found on the Internet by me unintentionally~~
The image is ready now, and the width and height of the Item have been set, that is, initItems
/** * Initialize each item * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12 */ private void initItem() { // Obtain the width of the Item int childWidth = (mWidth - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn; mItemWidth = childWidth; mGamePintuItems = new ImageView[mColumn * mColumn]; // Place 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); item.setTag(i + "_" + mItemBitmaps.get(i).index); RelativeLayout.LayoutParams lp = new LayoutParams(mItemWidth, mItemWidth); // Set horizontal margin, not the last column if ((i + 1) % mColumn != 0) { lp.rightMargin = mMargin; } // If it is not the first column lp.addRule(RelativeLayout.RIGHT_OF,// mGamePintuItems[i - 1].getId()); } // If it is not the first row,//Set vertical margin, not the last row if ((i + 1) > mColumn) { lp.addRule(RelativeLayout.BELOW,// mGamePintuItems[i - mColumn].getId()); } addView(item, lp); } }
As can be seen from our calculation of Item width: childWidth = (mWidth - mPadding 2 - mMargin (mColumn - 1) ) / The width of the container, excluding its own padding, excluding the spacing between Items, and then dividing by the number of Items per row, we get the width of the Item~~
Next, it is to iterate and generate Item, setting Rule based on their positions, please take a close look at the comments~~
Pay attention to two points:
1and we have set setOnClickListener for Item, of course, because our game is to click on Item, right~
2and we have set a Tag for Item: item.setTag(i + "_" + mItemBitmaps.get(i).index);
The tag stores the index, which is the correct position; and i, which can help us find the current item's image in mItemBitmaps: (mItemBitmaps.get(i).bitmap))
That's it, the code for our game's layout is finished~~~
Then we declare it in the layout file:
<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>
Remember to set this layout in the Activity~~
The current effect is:
Game switch effect
Initial switch
Remember that we added onClick listeners to the items~~
Now we need to implement the exchange of images when clicking two items
So, we need two member variables to store these two items and then swap them
/** * Record the ImageView of the first click */ private ImageView mFirst; /** * Record the ImageView of the second click */ private ImageView mSecond; /** * Click event * @param view the view * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12 */ @Override public void onClick(View v) { /** * If the two clicks are on the same item */ if (mFirst == v) { mFirst.setColorFilter(null); mFirst = null; return; } //Click the first item if (mFirst == null) { mFirst = (ImageView) v; mFirst.setColorFilter(Color.parseColor("#"));55FF0000")); } else//Click the second Item { mSecond = (ImageView) v; exchangeView(); } }
Click the first one, set the selected effect by calling setColorFilter, and click another one, then we are ready to call exchangeView to exchange the picture, of course, this method we haven't written yet, let's put it aside~
If you click the same one twice, remove the selected effect, we treat it as if nothing happened
Next, let's implement exchangeView:
/** * Exchange two item images * @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(); //Get the index position in the 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; }
You should remember our setTag from before, if you forget, go and take a look, we were still talking about it~
Through getTag, get the index in the List, then get the bitmap for exchange settings, and finally exchange the tag;
Up to now, our exchange effect is written, our game can be over~~
The effect is as follows:
As you can see, we can play now, as to why not use a fresh landscape picture, it is because, it is really hard to tell which part matches which, or the girl is more intuitive~
You must be complaining, my god, where is the animation switch? It should be two flying over to exchange positions, what is this?
Yes, we should have aspirations for the program, let's add the animation switch effect now~~
Seamless animation switching
Let's talk about how to add it first, I plan to use TranslationAnimation, and then get the top and left of the two Items from the container;
However, it is important to understand that in fact, the Item only changes the setImage, and the position of the Item does not change;
Now we need the animation moving effect, for example, A moves to B, no problem, after the movement is completed, the Item should go back, but the picture does not change, and we still need to manually call setImage;
This creates a phenomenon, the animation switch effect is there, but there will still be a flash at the end, caused by the switch of the image;
To avoid the above phenomenon and achieve a perfect switch effect, we introduce an animation layer here, which is dedicated to the animation effect, somewhat similar to the layers in Photoshop. Let's see how we do it below;
/** * Flag indicating whether the animation is running */ private boolean isAniming; /** * Animation layer */ private RelativeLayout mAnimLayout; /** * Exchange two item images * @author qiu Blog: www.qiuchengjia.cn Time:2016-09-12 */ private void exchangeView(){ mFirst.setColorFilter(null); setUpAnimLayout(); // Add 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); // Add 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); // Set animation 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); // Add animation listener anim.setAnimationListener(new AnimationListener(){ @Override public void onAnimationStart(Animation animation){ isAnimating = 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]); }
When starting the exchange, we create an animation layer, then add two identical Items on this layer, hide the original Item, and then carry out the animation switch freely, setFillAfter to true~
After the animation is completed, we have quietly exchanged the image of the Item and directly displayed it. This is a perfect switch:
The general process:
1A, B hide
2A copy animation moves to the position of B; B copy moves to the position of A
3A sets the image to B, removes the copy of B, and A is displayed, thus perfectly fitting, making the user feel that B has moved over
4Same as B
Now our effect:
Now the effect is satisfactory~~ To prevent users from clicking repeatedly, add a sentence in the onClick:
@Override public void onClick(View v) { // If the animation is being executed, then block 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 then perform the checkSuccess(); judgment; fortunately, we have stored the correct order of the images in the tag~~
/** * Used 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(); } } /** * 获得图片的真正索引 * @param tag * @return */ private int getIndexByTag(String tag){ String[] split = tag.split("_"); return Integer.parseInt(split[1 }
It's very simple, traverse all the Items, get the real index and compare with the natural order, if they are completely consistent, then victory~~ After victory, proceed to the next level
As for the code of the next level:
public void nextLevel(){ this.removeAllViews(); mAnimLayout = null; mColumn++; initBitmap(); initItem(); }
Summary
Alright, that's about it for the content we introduced in this article. Those who are interested can try it themselves. This will be more helpful for everyone's understanding and learning. If you have any questions, you can leave comments for discussion.