本Demo主要目的为学习及研究自定义ViewGroup,通过实现一种拼图游戏而熟悉ViewGroup的onMeasure,onLayout及onTouchEvent的处理
gitHub地址
游戏下载地址
游戏原型
- demo展示
实现说明及注意点
- 使用RelaytiveLayout,通过addView(ImageView.setImageBitmap(Bitmap))方式添加所有小方块,其中添加的View需设置合适的LayoutParams
1.根据RelaytiveLayout及所选择图片的宽高选择游戏区域,并创建各个小滑块
private List calcBitmap() {
if (TextUtils.isEmpty(mImagePath)) {
return null;
}
mBitmap = BitmapFactory.decodeFile(mImagePath);
List bms = new ArrayList<>();
int bw = mBitmap.getWidth();
int bh = mBitmap.getHeight();
float scale = 1;
if ((getWidth() * 1.0f) / (bw * 1.0f) > (getHeight() * 1.0f) / (bh * 1.0f)) {
scale = (getWidth() * 1.0f) / (bw * 1.0f);
} else {
scale = (getHeight() * 1.0f) / (bh * 1.0f);
}
Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
mBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(),
mBitmap.getHeight(), matrix, true);
bw = mBitmap.getWidth();
bh = mBitmap.getHeight();
if (bw * 1.f / bh > getWidth() * 1.f / getHeight()) {
mBitmap = Bitmap.createBitmap(mBitmap, (bw - getWidth()) / 2, 0, getWidth(), getHeight());
} else {
mBitmap = Bitmap.createBitmap(mBitmap, 0, (bh - getHeight()) / 2, getWidth(), getHeight());
}
for (int i = 0; i < mColumns * mColumns; i++) {
if (i != mColumns * mColumns - 1) {
bms.add(new Units(Bitmap.createBitmap(mBitmap, i % mColumns * getWidth() / mColumns, i / mColumns * getHeight() / mColumns, getWidth() / mColumns, getHeight() / mColumns), i));
} else {
sortBlocks(bms); //排序滑块
bms.add(new Units(Bitmap.createBitmap(getWidth() / mColumns, getHeight() / mColumns, Bitmap.Config.ALPHA_8), i));
}
}
return bms;
}
class Units {
Bitmap bm;
//用于判断滑块所在位置是否正确
int tag;
Units(Bitmap bm, int tag) {
this.bm = bm;
this.tag = tag;
}
}
2.计算所有滑块的所处的ViewGroup区域
private void initRects() {
mRects = new ArrayList<>();
int unitWidth = getWidth() / mColumns;
int unitHeight = getHeight() / mColumns;
for (int i = 0; i < mColumns * mColumns; i++) {
mRects.add(new Rect(i % mColumns * unitWidth
, i / mColumns * unitHeight
, i % mColumns * unitWidth + unitWidth
, i / mColumns * unitHeight + unitHeight));
}
}
3.创建ImageView并添加到ViewGroup中
for (int i = 0; i < mBms.size(); i++) {
ImageView imageView = new ImageView(getContext());
imageView.setImageBitmap(mBms.get(i).bm);
RelativeLayout.LayoutParams lp = new LayoutParams(getWidth() / mColumns, getHeight() / mColumns);
imageView.setLayoutParams(lp);
addView(imageView);
}
4.处理onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
if (event.getPointerCount() > 1 || TextUtils.isEmpty(mImagePath) || finished) {
return false;
}
mX = event.getX();
mY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
calcCurrentItem(); //计算当前触摸条目
break;
case MotionEvent.ACTION_MOVE:
calcOrientation(); //计算滑动方向
calcMoveAble(); //根据游戏规则判断触摸滑块是否可滑动
calcMoveFinish(); //判断滑块是否完成了滑动
if (!mMoveAble) {
return false;
}
requestLayout();
break;
case MotionEvent.ACTION_UP:
if (!mMoveAble) {
return false;
}
if (isMoveFinish) {
Collections.swap(mBms, mCurrentItem, mBlankItem);
resetChild();
} else {
calcNeedMoveBlock(); //根据手指移动距离判断是否需要在手指抬起时移动滑块
}
calcResult();
requestLayout();
break;
default:
break;
}
return true;
}
private void calcCurrentItem() {
if (TextUtils.isEmpty(mImagePath)) {
return;
}
for (int i = 0; i < mRects.size(); i++) {
if (mRects.get(i).contains((int) mDownX, (int) mDownY)) {
mCurrentItem = i;
return;
}
}
}
private void calcOrientation() {
if (Math.abs(mX - mDownX) > Math.abs(mY - mDownY) && mX - mDownX < 0) {
mMoveOrientation = 1;
} else if (Math.abs(mX - mDownX) > Math.abs(mY - mDownY) && mX - mDownX > 0) {
mMoveOrientation = 3;
} else if (Math.abs(mX - mDownX) < Math.abs(mY - mDownY) && mY - mDownY > 0) {
mMoveOrientation = 4;
} else {
mMoveOrientation = 2;
}
}
private void calcMoveAble() {
for (int i = 0; i < mBms.size(); i++) {
if (mBms.get(i).tag == mColumns * mColumns - 1) {
mBlankItem = i;
break;
}
}
if ((mBlankItem + 1) % mColumns == 0) {
switch (mMoveOrientation) {
case 2:
mMoveAble = mCurrentItem - mColumns == mBlankItem;
break;
case 3:
mMoveAble = mCurrentItem + 1 == mBlankItem;
break;
case 4:
mMoveAble = mCurrentItem + mColumns == mBlankItem;
break;
case 1:
default:
mMoveAble = false;
break;
}
} else if ((mBlankItem + 1) % mColumns == 1) {
switch (mMoveOrientation) {
case 1:
mMoveAble = mCurrentItem - 1 == mBlankItem;
break;
case 2:
mMoveAble = mCurrentItem - mColumns == mBlankItem;
break;
case 4:
mMoveAble = mCurrentItem + mColumns == mBlankItem;
break;
case 3:
default:
mMoveAble = false;
break;
}
} else {
switch (mMoveOrientation) {
case 1:
mMoveAble = mCurrentItem - 1 == mBlankItem;
break;
case 2:
mMoveAble = mCurrentItem - mColumns == mBlankItem;
break;
case 3:
mMoveAble = mCurrentItem + 1 == mBlankItem;
break;
case 4:
mMoveAble = mCurrentItem + mColumns == mBlankItem;
break;
default:
mMoveAble = false;
break;
}
}
}
private void calcMoveFinish() {
switch (mMoveOrientation) {
case 1:
case 3:
isMoveFinish = Math.abs(mX - mDownX) > mRects.get(mBlankItem).right - mRects.get(mBlankItem).left;
break;
case 2:
case 4:
isMoveFinish = Math.abs(mY - mDownY) > mRects.get(mBlankItem).bottom - mRects.get(mBlankItem).top;
break;
default:
break;
}
}
private void calcNeedMoveBlock() {
switch (mMoveOrientation) {
case 1:
case 3:
isMoveFinish = Math.abs(mX - mDownX) * 3 > Math.abs(mRects.get(mCurrentItem).right - mRects.get(mCurrentItem).left);
break;
case 2:
case 4:
isMoveFinish = Math.abs(mY - mDownY) * 3 > Math.abs(mRects.get(mCurrentItem).bottom - mRects.get(mCurrentItem).top);
break;
default:
break;
}
if (isMoveFinish) {
Collections.swap(mBms, mCurrentItem, mBlankItem);
resetChild();
} else {
resetChild();
}
}
5.重写onMeasure及onLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == AT_MOST && heightSpecMode == AT_MOST) {
setMeasuredDimension(mMinSize, mMinSize);
} else if (widthMeasureSpec == AT_MOST) {
setMeasuredDimension(mMinSize, heightSpecSize);
} else if (heightMeasureSpec == AT_MOST) {
setMeasuredDimension(widthSpecSize, mMinSize);
}
measureChild();
}
private void measureChild() {
for (int i = 0; i < getChildCount(); i++) {
if (i == mColumns * mColumns) {
getChildAt(i).measure(getWidth(), getHeight());
return;
}
getChildAt(i).measure(getWidth() / mColumns - mSeparatorWidth, getHeight() / mColumns - mSeparatorWidth);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
for (int i = 0; i < getChildCount(); i++) {
if (i == mColumns * mColumns) {
getChildAt(i).layout(0, 0, getWidth(), getHeight());
return;
}
if (mCurrentItem == i) {
if (isMoveFinish) {
getChildAt(i).layout(mRects.get(mBlankItem).left + mSeparatorWidth / 2
, mRects.get(mBlankItem).top + mSeparatorWidth / 2
, mRects.get(mBlankItem).right - mSeparatorWidth / 2
, mRects.get(mBlankItem).bottom - mSeparatorWidth / 2);
} else {
switch (mMoveOrientation) {
case 1:
getChildAt(i).layout((int) (mX - mDownX + mRects.get(mCurrentItem).left) + mSeparatorWidth / 2
, mRects.get(mCurrentItem).top + mSeparatorWidth / 2
, (int) (mX - mDownX + mRects.get(mCurrentItem).right) - mSeparatorWidth / 2
, mRects.get(mCurrentItem).bottom - mSeparatorWidth / 2);
break;
case 3:
getChildAt(i).layout((int) (mX - mDownX + mRects.get(mCurrentItem).left) + mSeparatorWidth / 2
, mRects.get(mCurrentItem).top + mSeparatorWidth / 2
, (int) (mX - mDownX + mRects.get(mCurrentItem).right) - mSeparatorWidth / 2
, mRects.get(mCurrentItem).bottom - mSeparatorWidth / 2);
break;
case 2:
getChildAt(i).layout(mRects.get(mCurrentItem).left + mSeparatorWidth / 2
, (int) (mY - mDownY + mRects.get(mCurrentItem).top) + mSeparatorWidth / 2
, mRects.get(mCurrentItem).right - mSeparatorWidth / 2
, (int) (mY - mDownY + mRects.get(mCurrentItem).bottom) - mSeparatorWidth / 2);
break;
case 4:
getChildAt(i).layout(mRects.get(mCurrentItem).left + mSeparatorWidth / 2
, (int) (mY - mDownY + mRects.get(mCurrentItem).top) + mSeparatorWidth / 2
, mRects.get(mCurrentItem).right - mSeparatorWidth / 2
, (int) (mY - mDownY + mRects.get(mCurrentItem).bottom) - mSeparatorWidth / 2);
break;
default:
getChildAt(i).layout(mRects.get(i).left + mSeparatorWidth / 2
, mRects.get(i).top + mSeparatorWidth / 2
, mRects.get(i).right - mSeparatorWidth / 2
, mRects.get(i).bottom - mSeparatorWidth / 2);
break;
}
}
} else {
getChildAt(i).layout(mRects.get(i).left + mSeparatorWidth / 2
, mRects.get(i).top + mSeparatorWidth / 2
, mRects.get(i).right - mSeparatorWidth / 2
, mRects.get(i).bottom - mSeparatorWidth / 2);
}
}
}
6.对外暴露的接口
public void showResult() {
int childCount = getChildCount();
if (childCount == mColumns * mColumns + 1 || TextUtils.isEmpty(mImagePath)) {
return;
}
ImageView imageView = new ImageView(getContext());
imageView.setImageBitmap(mBitmap);
RelativeLayout.LayoutParams lp = new LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
imageView.setLayoutParams(lp);
addView(imageView);
invalidate();
}
public void hideResult() {
if (TextUtils.isEmpty(mImagePath)) {
return;
}
removeViewAt(getChildCount() - 1);
invalidate();
}
public void setEasier() {
if (TextUtils.isEmpty(mImagePath) || mColumns <= 3) {
return;
}
mColumns -= 1;
setImage(mImagePath);
}
public void setHarder() {
if (TextUtils.isEmpty(mImagePath) || mColumns >= 5) {
return;
}
mColumns += 1;
setImage(mImagePath);
}
public interface FinishListener {
void finish(long mills);
}
public void addFinishListener(FinishListener listener) {
mListener = listener;
}
- 难点:简单的使用ColleCtions.suffle()打乱各个方块将有一半的几率导致游戏无法完成,需使用Collections.swap(int i,int j)方式打乱各模块
private void sortBlocks(List bms) {
if (mColumns % 2 == 0) {
for (int i = 0; i < 40; i++) {
int index = (int) (Math.random() * bms.size());
if (index % mColumns + 2 > mColumns) {
if (index - 2 >= 0) {
Collections.swap(bms, index, index - 2);
} else {
Collections.swap(bms, index, index + mColumns * 2);
}
} else {
if (index + 2 < bms.size()) {
Collections.swap(bms, index, index + 2);
} else {
Collections.swap(bms, index, index - mColumns * 2);
}
}
}
} else {
for (int i = 0; i < 60; i++) {
int index = (int) (Math.random() * bms.size());
if (index + 2 >= bms.size()) {
Collections.swap(bms, index, index - 2);
} else {
Collections.swap(bms, index, index + 2);
}
}
}
}
- 错误示例(相邻两个滑块直接进行奇数数次交换位置,最后将会剩下两个滑块位置颠倒,且无法通过移动滑块位置还原)
- 正确示例
游戏说明
- 可以通过easy和hard减少和增加游戏难度(最易为3x3难度,最难为5x5难度)
- 可以通过按住showpic按钮显示完成后效果图片
其他说明
- 使用了RxPermission对不同版本手机进行了拍照及文件读取权限处理
- 使用了ImagePicker图片选择框架选择手机内存图片或拍照