前言:用android原生的viewgroup和view,再加上2048内核的逻辑,手搓小游戏,仅仅是个人学习爱好记录
一、项目结构
EntityNumView.kt:2048游戏数字块的自定义view,继承原生的view来二次手搓。
TouchView.kt:触摸面板View,继承viewGroup,里面实现了view的移动动效、合并、view的添加。
MainBackView:背景图,和TouchView分隔开了,手搓的背景图,不过可以和TouchView合并优化。
Two028Helper:2048逻辑类,主要是数字矩阵的合并判断啥的,比较简单。
Main2028Activity:主界面Activity展示类,没啥好说的
二、2048内核逻辑解析
内核逻辑其实很简单,就是一个N*N的矩阵,对矩阵进行变换罢了。我们将问题进行拆分:
数组合并挤压
其实实际上每次滑动都是对矩阵内所有数字往同一个方向进行挤压计算合并,所以实际上就是一行或者一列数字的逻辑计算罢了。
我们这边以一个数字为例表示,并往左侧方向挤压:
[0,8,0,8]——>[16,0,0,0]
[0,2,2,4]——>[4,4,0,0]
[0,2,4,0]——>[2,4,0,0]
其实逻辑很简单,就是相同的数字进行合并,并且只能合并一次。
整体逻辑很简单,分为两步
步骤a:首先我们对非0的数字进行判断,非0数字array[i]的下一个非0数字array[j],如果相同,那么array[j]=array[i]*2,同时array[i]=0(以往左挤压为例)
[0,8,0,8]——>步骤a变换后:[0,16,0,0]
步骤b:对步骤a变换后的矩阵,进行移动,逻辑也很简单,循环计算0的个数,移动到非0的时候,则array[i-n]=array[i],n表示前面计算的非0个数
[0,16,0,0]——>步骤b变换后:[16,0,0,0]
上述逻辑代码我就不贴了,逻辑很简单。
增加随机数
每次移动完毕后,需要我们随机生成一个新数字添加到空白的格子中,假设是n*n矩阵,那么我们只要循环遍历这个矩阵,将所有空白的位置记录下来,保存在一个数组list中,然后生成一个小于数组长度的随机数i,得到list[i],这个就是添加的位置了。
fun getEmptyPosList(): MutableList<Int> {
val resultList: MutableList<Int> = mutableListOf()
for (i in entityNumArray.indices) {
for (j in entityNumArray[i].indices) {
if (entityNumArray[i][j] == 0) {
resultList.add(i*size + j)
}
}
}
return resultList
}
//
val emptyList = logicHelper.getEmptyPosList()
if (emptyList.isEmpty()) {
return
}
addPos = emptyList[Random.nextInt(0, emptyList.size)]
三、android原生控件手搓
ok现在我们进入到下一步,通过android的原生控件进行手搓,这同时也是整个项目中难度最大最大的地方,因为不像2048的逻辑一样只要关心数字矩阵变换就可以,这里涉及到view的绘制、移动动效、合并view怎么remove等等等,是整个项目中难度最大的!
这里我只讲一下TouchView.kt和logicHelper.kt的逻辑好了。
(1)TouchView中判断移动方向
很简单:根据ontouchEvent回调中得到dy和dx,然后根据这个值来做判断到底是上下左右怎么移动
override fun onTouchEvent(event: MotionEvent?): Boolean {
//...
when(event?.action){
MotionEvent.ACTION_DOWN -> {
firstTouchX = event.x
firstTouchY = event.y
}
MotionEvent.ACTION_MOVE -> {
deltaX = event.x - firstTouchX
deltaY = event.y - firstTouchY
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) >= 20) {
if (deltaX > 0) {
startToRight()
} else {
startToLeft()
}
//...
} else if(Math.abs(deltaX) < Math.abs(deltaY) && Math.abs(deltaY) >= 20) {
if (deltaY > 0) {
startToBottom()
} else {
startToTop()
}
//...
}
}
}
return true
}
(2)对添加上去的view展示
首先我们都了解view的绘制原理,因为我是完全继承viewgroup的,所以需要重写onMeasure——这个用于计算子view,否则子view的长宽显示异常;重写onLayout,计算子view真正的位置——这个用于新增数字方块时,展示的位置
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
gridSize = (this.measuredWidth - 40F - (size+1)*paintStrokeWidth)/size //每一个栅格尺寸,40F是边距(30F-10F线条一半,然后×2)
val spec = MeasureSpec.makeMeasureSpec(gridSize.toInt(),MeasureSpec.EXACTLY)
val count = childCount
for (i in 0 until count) {
//这个很重要,没有就不显示,自定义view一定需要!!!!,子view大小就是在这里这是的,measure->onMeasure->set尺寸
getChildAt(i).measure(spec, spec)
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (byAdd) {
val row = addPos/size
val col = addPos%size
val left = (col+1)*paintStrokeWidth + col*gridSize + paintStrokeWidth //线条宽度+栅格+边距
val top = (row+1)*paintStrokeWidth + row*gridSize + paintStrokeWidth //线条宽度+栅格+边距
if (childCount == 0) {
return
}
val lastChildView = getChildAt(childCount-1)
lastChildView.layout(left.toInt(), top.toInt(), (left+gridSize).toInt(),
(top+gridSize).toInt()
)
byAdd = false
} else {
//todo nothing
}
}
(3)不做合并的情况下移动移动view
不做合并的情况下移动view其实比较简单,我们只要判断非0元素下,前面的0元素个数,那么就是移动的方块距离了,注意由于属性动画没有真正改变位置,所以移动完毕后,需要重新view.layout()设置布局参数,同时translationX/Y=0,恢复动画样式。
private fun startDoMove(view: View, dis: Int, dir: Direction, mFun: (() -> Unit)? = null) {
var animation = ObjectAnimator.ofFloat(view,"translationX",0f,300f)
animation.duration = 200
val moveDis = dis * (gridSize+paintStrokeWidth)
when(dir) {
Direction.UP -> {
animation = ObjectAnimator.ofFloat(view,"translationY",0f,-moveDis)
}
Direction.DOWN -> {
animation = ObjectAnimator.ofFloat(view,"translationY",0f,moveDis)
}
Direction.LEFT -> {
animation = ObjectAnimator.ofFloat(view,"translationX",0f,-moveDis)
}
Direction.RIGHT -> {
animation = ObjectAnimator.ofFloat(view,"translationX",0f,moveDis)
}
}
animation.start()
animation.addListener(object :Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
animationFinish = false
}
override fun onAnimationEnd(animation: Animator) {
animationFinish = true
//重新设置view的位置
when(dir) {
Direction.UP -> {
view.layout(view.left, view.y.toInt(), view.right, (view.y + gridSize).toInt())
//手动重制
view.translationY = 0F
mFun?.invoke()
}
Direction.DOWN -> {
view.layout(view.left, view.y.toInt(), view.right, (view.y + gridSize).toInt())
view.translationY = 0F
mFun?.invoke()
}
Direction.LEFT -> {
view.layout(view.x.toInt(), view.top, (view.x + gridSize).toInt(), view.bottom)
view.translationX = 0F
mFun?.invoke()
}
Direction.RIGHT -> {
view.layout(view.x.toInt(), view.top, (view.x + gridSize).toInt(), view.bottom)
view.translationX = 0F
mFun?.invoke()
}
}
}
override fun onAnimationCancel(animation: Animator) {
}
override fun onAnimationRepeat(animation: Animator) {
}
})
}
所以根据上述的逻辑来看,我们需要维护一个移动距离的数组,我们假设按照这个[0,8,0,4]的样例来,那么往左移动数组就是[-1,1,-1,3],往右移动数组就是[-1,2,-1,0]
(4)做合并的情况下移动移动view
项目中最难最难的一个点,也是花费我时间最长的点,因为这个涉及到view合并时何时remove、remove掉那个相同元素、view引用的问题等等等
上文在讲2048内核逻辑的时候其实很简单,对于相同元素合并,只要分为两步骤1.合并不移动,2.移动。但是这里又要涉及到view移动动效和移动距离了,难点在于记录移动距离。我们以[0,8,0,8]向左合并为例讲一下逻辑细节:
我们可以很简单的看出来,最终合并的结果是[16,0,0,0],那么反推就是第一个8需要移动1格,第二个8需要移动3格。其实上文2048内核逻辑中我所说的合并元素分为两步走里,是可以记录到移动数组的,我们只要在合并阶段,将0的计数+1即可。我们来看逻辑代码:
public static void main(String[] args) {
int[] arg = {0,8,0,8};
///结果4,8,4,16,16,0,0,0,0,0,0,0<---移动
int[] canMoved = new int[arg.length]; //最终移动的pos
int dis = 0;
int lastDataPos = 0;
for (int i=0; i<arg.length; i++) {
if (arg[i] == 0) {
dis ++;
} else {
canMoved[i] = dis;
if (arg[lastDataPos] == arg[i] && arg[i] != 0 && i != 0) {
//开始合并消星
arg[lastDataPos] = arg[lastDataPos] * 2;
arg[i] = 0;
dis ++;
canMoved[i] = canMoved[i] + 1; //重点,这里距离需要+1,因为自己已经是0了
}
lastDataPos = i;
}
}
}
上述代码最终得到的canMoved=[-1,1,-1,3]。好,距离矩阵的逻辑我们得到了,那么移动的逻辑也很简单了。
那么下一步就是这么removeView了,应该review哪一个相同的view呢?
由于这是android,所以不仅仅单纯的需要维护内容数组、移动数组,还需要维护一个view数组(可以根据当前i、j得到view,然后做view的移动动画),当然了view数组并不是保存view对象,而是保存的是view的引用(可以说是指针,这里不具体展开为什么不是保存view对象了,这个讲起来非常复杂)。这里还是以[0,8,0,8]为例,向左挤压的时候,view该怎么消除,保存哪一个view:
首先我们确保的一个事情就是,我们无论remove哪个8,都要确保另外一个8都能被listView[i][j]引用到,这就是问题的核心所在!之前debug问题的时候,发现view引用不到了,这个view就在界面上孤立无援,动都动不了┭┮﹏┭┮!
当开始合并完,第二步开始移动的时候,我们会对值进行覆盖,那么listView[i]=null就很容易造成引用链丢失
fun startToLeft() {
//步骤1:对非0的相邻的数字判断,如果相同就合并,并将后一位置为0,并生成canMove,canMove用于动画计算
for (i in entityNumArray.indices){
var lastDataPos = 0
var count = 0
var needMerge = false
for (j in entityNumArray[0].indices) {
needMerge = false
var tempRemoveView: View? = null
if (entityNumArray[i][j] != 0) {
canMove[i][j] = count
if (entityNumArray[i][lastDataPos] == entityNumArray[i][j] && j != 0) {
needMerge = true
//开始合并消星
entityNumArray[i][lastDataPos] = entityNumArray[i][lastDataPos] * 2
entityNumArray[i][j] = 0
count++ //消一次星,相当于多出来一个0,所以需要count++
canMove[i][j] = canMove[i][j] + 1
//引用view,优先保存住!
tempRemoveView = listView[i][lastDataPos] //假设0.8.0.8,那么这里的tempRemoveView是第一个8
}
lastDataPos = j
} else {
count++
canMove[i][j] = -1
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
if (needMerge) {
logicCallback?.startDoMoveAndMerge(listView[i][j]!!,canMove[i][j], Direction.LEFT,tempRemoveView,i,j)
} else {
logicCallback?.startDoMove(listView[i][j]!!,canMove[i][j], Direction.LEFT)
}
}
}
}
//步骤一结束后,假设是0,8,0,8。那么entityNumArray这一行对应的是0,16,0,0,canMove这一行对应-1,1,-1,3
//步骤二:完全往前移动,是最终的结果,包括了移动和合并
for (i in entityNumArray.indices) {
var tempCount = 0
for (j in entityNumArray[0].indices) {
if (entityNumArray[i][j] != 0) {
if (tempCount != 0) {
entityNumArray[i][j-tempCount] = entityNumArray[i][j]
entityNumArray[i][j] = 0
}
} else {
tempCount++
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
listView[i][j - canMove[i][j]] = listView[i][j]
listView[i][j] = null
/**
* 假设0.8.0.8这样的:我们怎么判断removeView是第一个8还是第二个8呢?
* 假设remove的是第一个8,那么listView[0]=listView[1],listView[1]=null;然后listView[0]=listView[3],listView[3]=null
* 如果是listView[1] 即是第一个8 remove掉的,实际上不影响,因为最后ListView[0]还是有值的。
* 如果是第二个8 remove掉的(listView[3]就是第二个8),那么最后listView[0]=listView[3],listView[3]=null,这个逻辑里,listView[3]就是空引用,listView[0]虽然还存在,但是有效引用listView[1]被空引用覆盖,
* 此时view还是存在的,但是引用没了,这个view就是孤立无援的,谁也拿不到了!!!!
*/
}
}
}
}
在上述步骤2阶段,[0,8,0,8]的move矩阵[-1,1,-1,3],数字矩阵就是[0,16,0,0]。开始移动:首先就是
listView[0]=listView[1],listView[1]=null;然后就是listView[0]=listView[3],listView[3]=null。我们可以看到最后实际上是listView[3]的引用会覆盖listView[1]的引用,即最后listView[0]的引用是listView[3]的。那么如果我们remove的是第二个8即listView[3],那么最终listView[0]的引用就是空的(这个view都已经被remove掉了),最后导致合并后的view谁也引用不到,孤零零的在界面上独自展示,再也移动不了了!
上图中最终listView[0]引用到的就是listView[3]的引用!
当然了想要移除第二个8即listView[3],那么我们在步骤2的逻辑里从后往前推遍历就行~
四、效果展示
2048demo
五、总结
1.2048的核心逻辑其实不难,主要是和android怎么配合的问题,android涉及到动效、view绘制,还要考虑到view数量异常
2.后续可以对整体结构继续优化:增加设计模式(目前想引入责任链模式)、移动结束后增加新方块这边直接强制了210ms,可以优化成动画结束回调、增加自定义的矩阵数量
附:源码
class EntityNumView : View {
private val paint = Paint()
private var text: String = ""
private val textPaint = Paint()
constructor(context: Context):super(context){
}
constructor(context: Context,num: Int):super(context){
this.text = num.toString()
}
constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet) {
}
constructor(context: Context,attributeSet: AttributeSet,defStyle:Int):super(context,attributeSet,defStyle) {
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
when(text) {
"2" -> {
paint.setColor(resources.getColor(R.color.firebrick))
}
"4" -> {
paint.setColor(resources.getColor(R.color.indigo))
}
"8" -> {
paint.setColor(resources.getColor(R.color.hot_pint))
}
"16" -> {
paint.setColor(resources.getColor(R.color.chartresuse))
}
"32" -> {
paint.setColor(resources.getColor(R.color.blue))
}
"64" -> {
paint.setColor(resources.getColor(R.color.gold))
}
else -> {
paint.setColor(resources.getColor(R.color.light_red))
}
}
paint.style = Paint.Style.FILL
//画圆角出现没有画全的情况,要考虑下是不是this.measuredWidth不正常,超过画布了,这个值都是父布局的onmeasure传过来的
canvas.drawRoundRect(0F,0F, this.measuredWidth.toFloat(),
this.measuredHeight.toFloat(),30F,30F,paint)
textPaint.setColor(Color.WHITE)
textPaint.textSize = 80F
val length = textPaint.measureText(text)
canvas.drawText(text, (this.measuredWidth - length)/2,
150F,textPaint)
}
private fun setText(s: String) {
text = s
invalidate()
}
fun getInt(): Int {
return text.toInt()
}
fun setInt(n: Int) {
setText(n.toString())
}
override fun toString(): String {
return "numView:$text"
}
init {
setText("屙屎测试测试")
}
}
class Two028Helper {
companion object {
var size = 4 //不要超太多,4或者5
var paintStrokeWidth = 20F
}
var entityNumArray :Array<IntArray> = Array(size) {IntArray(size)} //实际数字矩阵
var canMove :Array<IntArray> = Array(size) {IntArray(size)} //数字矩阵对应的可以移动的距离
var listView: Array<Array<View?>> = Array(size) { arrayOfNulls<View?>(size) } //对应的view矩阵
var logicCallback: DoViewLogic? = null
private fun logGetEntityNumArray() {
Log.d("chen","-----------------------")
for (i in entityNumArray.indices) {
Log.d("chen","entityNumArray:${entityNumArray[i].contentToString()}")
}
Log.d("chen","-----------------------")
}
private fun logGetCanMove() {
Log.d("chen","-----------------------")
for (i in canMove.indices) {
Log.d("chen","canMove:${canMove[i].contentToString()}")
}
Log.d("chen","-----------------------")
}
private fun logGetListView() {
Log.d("chen","-----------------------")
for (i in listView.indices) {
Log.d("chen","listView:${listView[i].contentToString()}")
}
Log.d("chen","-----------------------")
}
fun initCanMove() {
for (i in canMove.indices) {
for (j in canMove[0].indices) {
canMove[i][j] = -1
}
}
}
fun getEmptyPosList(): MutableList<Int> {
val resultList: MutableList<Int> = mutableListOf()
for (i in entityNumArray.indices) {
for (j in entityNumArray[i].indices) {
if (entityNumArray[i][j] == 0) {
resultList.add(i*size + j)
}
}
}
return resultList
}
fun startToLeft() {
Log.d("chen","move before")
logGetEntityNumArray()
//步骤1:对非0的相邻的数字判断,如果相同就合并,并将后一位置为0,并生成canMove,canMove用于动画计算
for (i in entityNumArray.indices){
var lastDataPos = 0
var count = 0
var needMerge = false
for (j in entityNumArray[0].indices) {
needMerge = false
var tempRemoveView: View? = null
if (entityNumArray[i][j] != 0) {
canMove[i][j] = count
if (entityNumArray[i][lastDataPos] == entityNumArray[i][j] && j != 0) {
needMerge = true
//开始合并消星
entityNumArray[i][lastDataPos] = entityNumArray[i][lastDataPos] * 2
entityNumArray[i][j] = 0
count++ //消一次星,相当于多出来一个0,所以需要count++
canMove[i][j] = canMove[i][j] + 1
//引用view,优先保存住!
tempRemoveView = listView[i][lastDataPos] //假设0.8.0.8,那么这里的tempRemoveView是第一个8
}
lastDataPos = j
} else {
count++
canMove[i][j] = -1
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
if (needMerge) {
logicCallback?.startDoMoveAndMerge(listView[i][j]!!,canMove[i][j], Direction.LEFT,tempRemoveView,i,j)
} else {
logicCallback?.startDoMove(listView[i][j]!!,canMove[i][j], Direction.LEFT)
}
}
}
}
//步骤一结束后,假设是0,8,0,8。那么entityNumArray这一行对应的是0,16,0,0,canMove这一行对应-1,1,-1,3
//步骤二:完全往前移动,是最终的结果,包括了移动和合并
for (i in entityNumArray.indices) {
var tempCount = 0
for (j in entityNumArray[0].indices) {
if (entityNumArray[i][j] != 0) {
if (tempCount != 0) {
entityNumArray[i][j-tempCount] = entityNumArray[i][j]
entityNumArray[i][j] = 0
}
} else {
tempCount++
}
//注意,这里需要用到canmove,因为listview不能通过上述tempCount来重新设置引用
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
listView[i][j - canMove[i][j]] = listView[i][j]
listView[i][j] = null
/**
* 假设0.8.0.8这样的:我们怎么判断removeView是第一个8还是第二个8呢?
* 假设remove的是第一个8,那么listView[0]=listView[1],listView[1]=null;然后listView[0]=listView[3],listView[3]=null
* 如果是listView[1] 即是第一个8 remove掉的,实际上不影响,因为最后ListView[0]还是有值的。
* 如果是第二个8 remove掉的(listView[3]就是第二个8),那么最后listView[0]=listView[3],listView[3]=null,这个逻辑里,listView[3]就是空引用,listView[0]虽然还存在,但是有效引用listView[1]被空引用覆盖,
* 此时view还是存在的,但是引用没了,这个view就是孤立无援的,谁也拿不到了!!!!
*/
}
}
}
Log.d("chen","move after")
logGetEntityNumArray()
}
fun startToRight() {
Log.d("chen","move before")
logGetEntityNumArray()
//步骤1:得到canMove
for(i in entityNumArray.indices) {
var count = 0
var lastPos = entityNumArray[0].size-1
var needMerge = false
for (j in entityNumArray[0].size-1 downTo 0) {
needMerge = false
var tempRemoveView: View? = null
if (entityNumArray[i][j] != 0) {
canMove[i][j] = count
//0,8,0,8
if (entityNumArray[i][j] == entityNumArray[i][lastPos] && j != entityNumArray[0].size-1) {
//可以合并
needMerge = true
entityNumArray[i][lastPos] = entityNumArray[i][lastPos] * 2
entityNumArray[i][j] = 0
//listView[i][lastPos],一定是lastPos而不是j,因为保存的是上一个view,如果保存当前view,那么会移除当前view,那么上一个tempView你去哪里获取呢?会被覆盖掉
//参考下面逻辑A
tempRemoveView = listView[i][lastPos]
count++
canMove[i][j] = canMove[i][j] + 1
}
lastPos = j
} else {
canMove[i][j] = -1
count ++
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
if (needMerge) {
logicCallback?.startDoMoveAndMerge(listView[i][j]!!,canMove[i][j], Direction.RIGHT,tempRemoveView,i,j)
} else {
logicCallback?.startDoMove(listView[i][j]!!,canMove[i][j],Direction.RIGHT)
}
}
}
}
//步骤二:完全往前移动,是最终的结果,包括了移动和合并
for (i in entityNumArray.indices) {
var tempCount = 0
for (j in entityNumArray[0].size-1 downTo 0) {
if (entityNumArray[i][j] != 0) {
if (tempCount != 0) {
entityNumArray[i][j+tempCount] = entityNumArray[i][j]
entityNumArray[i][j] = 0
}
} else {
tempCount++
}
//比如8.0.8.0,最后变成0,0,0,16,先是list[3]=list[2],然后list[3]=list[0],list[2]的引用会被覆盖再也找不到,所以需要remove这个,tempRemove记录的也是这个
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
listView[i][j + canMove[i][j]] = listView[i][j]
listView[i][j] = null
}
}
}
Log.d("chen","move after")
logGetEntityNumArray()
}
fun startToTop() {
Log.d("chen","move before")
logGetEntityNumArray()
for (j in entityNumArray[0].indices) {
var count = 0
var lastPos = 0
var canMerge: Boolean
for (i in entityNumArray.indices) {
canMerge = false
var tempRemoveView: View? = null
if (entityNumArray[i][j] != 0) {
canMove[i][j] = count
if (entityNumArray[i][j] == entityNumArray[lastPos][j] && i != 0) {
canMerge = true
entityNumArray[lastPos][j] = entityNumArray[lastPos][j] * 2
entityNumArray[i][j] = 0
count++
canMove[i][j] = canMove[i][j] + 1
tempRemoveView = listView[lastPos][j]
}
lastPos = i
} else {
canMove[i][j] = -1
count ++
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
if (canMerge) {
logicCallback?.startDoMoveAndMerge(listView[i][j]!!,count,Direction.UP,tempRemoveView,i,j)
} else {
logicCallback?.startDoMove(listView[i][j]!!,count,Direction.UP)
}
}
}
}
//步骤二:完
for (j in entityNumArray[0].indices) {
var tempCount = 0
for (i in entityNumArray.indices) {
if (entityNumArray[i][j] != 0) {
if (tempCount != 0) {
entityNumArray[i-tempCount][j] = entityNumArray[i][j]
entityNumArray[i][j] = 0
}
} else {
tempCount++
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
listView[i-canMove[i][j]][j] = listView[i][j]
listView[i][j] = null
}
}
}
Log.d("chen","move after")
logGetEntityNumArray()
}
fun startToBottom() {
Log.d("chen","move before")
logGetEntityNumArray()
//获取每一个item的下面一共多少个空格
for (j in entityNumArray[0].indices) {
var count = 0
var needMerge = false
var lastPos = entityNumArray.size-1
for (i in entityNumArray.size-1 downTo 0) {
needMerge = false
var tempRemoveView: View? = null
if (entityNumArray[i][j] != 0) {
canMove[i][j] = count
if (entityNumArray[i][j] == entityNumArray[lastPos][j] && i != entityNumArray.size-1) {
needMerge = true
entityNumArray[lastPos][j] = entityNumArray[i][j] * 2
entityNumArray[i][j] = 0
count++
canMove[i][j] = canMove[i][j] + 1
tempRemoveView = listView[lastPos][j]
}
lastPos = i
} else {
canMove[i][j] = -1
count ++
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
if (needMerge) {
logicCallback?.startDoMoveAndMerge(listView[i][j]!!,count,Direction.DOWN,tempRemoveView,i,j)
} else {
logicCallback?.startDoMove(listView[i][j]!!,count,Direction.DOWN)
}
}
}
}
for (j in entityNumArray[0].indices) {
var temp = 0
for (i in entityNumArray.size-1 downTo 0) {
if (entityNumArray[i][j] != 0) {
if (temp != 0) {
entityNumArray[i+temp][j] = entityNumArray[i][j]
entityNumArray[i][j] = 0
}
} else {
temp ++
}
if (canMove[i][j] != 0 && canMove[i][j] != -1) {
listView[i+canMove[i][j]][j] = listView[i][j]
listView[i][j] = null
}
}
}
Log.d("chen","move after")
logGetEntityNumArray()
}
fun getFirstCanMovePoe(list: IntArray):Int {
for (i in list.indices) {
if (list[i] != 0 && list[i] != -1)
return i
}
return -1
}
fun getLastCanMovePoe(list: IntArray):Int {
for (i in list.size-1 downTo 0) {
if (list[i] != 0 && list[i] != -1)
return i
}
return -1
}
fun setCallback(callback: DoViewLogic) {
this.logicCallback = callback
}
interface DoViewLogic {
fun startDoMoveAndMerge(view: View, move: Int, dir: Direction, removeView: View?,i: Int, j: Int)
fun startDoMove(view: View, move: Int, dir: Direction)
}
}
//触碰的view布局
class TouchView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
): ViewGroup(context,attrs,defStyleAttr) {
private var addPos = 0
private var logicHelper = Two028Helper()
private var firstTouchX = 0F
private var firstTouchY = 0F
private var deltaX = 0F
private var deltaY = 0F
private var gridSize = 0F
private var animationFinish = true
private var byAdd = false //是不是新添加
enum class Direction{
UP,
DOWN,
LEFT,
RIGHT
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
gridSize = (this.measuredWidth - 40F - (size+1)*paintStrokeWidth)/size //每一个栅格尺寸,40F是边距(30F-10F线条一半,然后×2)
val spec = MeasureSpec.makeMeasureSpec(gridSize.toInt(),MeasureSpec.EXACTLY)
val count = childCount
for (i in 0 until count) {
//这个很重要,没有就不显示,自定义view一定需要!!!!,子view大小就是在这里这是的,measure->onMeasure->set尺寸
getChildAt(i).measure(spec, spec)
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (byAdd) {
val row = addPos/size
val col = addPos%size
val left = (col+1)*paintStrokeWidth + col*gridSize + paintStrokeWidth //线条宽度+栅格+边距
val top = (row+1)*paintStrokeWidth + row*gridSize + paintStrokeWidth //线条宽度+栅格+边距
if (childCount == 0) {
return
}
val lastChildView = getChildAt(childCount-1)
lastChildView.layout(left.toInt(), top.toInt(), (left+gridSize).toInt(),
(top+gridSize).toInt()
)
byAdd = false
} else {
//todo nothing
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (!animationFinish) {
return true
}
when(event?.action){
MotionEvent.ACTION_DOWN -> {
firstTouchX = event.x
firstTouchY = event.y
}
MotionEvent.ACTION_MOVE -> {
deltaX = event.x - firstTouchX
deltaY = event.y - firstTouchY
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) >= 20) {
if (deltaX > 0) {
startToRight()
} else {
startToLeft()
}
handler.postDelayed({
addNextView() //优化一下,等动效全部做完
},210)
} else if(Math.abs(deltaX) < Math.abs(deltaY) && Math.abs(deltaY) >= 20) {
if (deltaY > 0) {
startToBottom()
} else {
startToTop()
}
handler.postDelayed({
addNextView()
},210)
}
}
}
return true
}
//初始化矩阵
private fun randomInit() {
addPos = Random.nextInt(0, size*size)
val row = addPos / size
val col = addPos % size
logicHelper.entityNumArray[row][col] = 2 //默认随机是2
}
private fun initLayout() {
randomInit()
addFirstView()
}
private fun addFirstView(num: Int=2) {
val view = EntityNumView(context,num)
byAdd = true
addView(view)
val row = addPos / size
val col = addPos % size
logicHelper.listView[row][col] = view
}
private fun addNextView() {
val emptyList = logicHelper.getEmptyPosList()
if (emptyList.isEmpty()) {
return
}
addPos = emptyList[Random.nextInt(0, emptyList.size)]
val row = addPos / size
val col = addPos % size
val random2N = 2 shl Random.nextInt(1, 3)
logicHelper.entityNumArray[row][col] = random2N
val view = EntityNumView(context,random2N)
logicHelper.listView[row][col] = view
byAdd = true
addView(view)
}
private fun startDoMove(view: View, dis: Int, dir: Direction, mFun: (() -> Unit)? = null) {
var animation = ObjectAnimator.ofFloat(view,"translationX",0f,300f)
animation.duration = 200
val moveDis = dis * (gridSize+paintStrokeWidth)
when(dir) {
Direction.UP -> {
animation = ObjectAnimator.ofFloat(view,"translationY",0f,-moveDis)
}
Direction.DOWN -> {
animation = ObjectAnimator.ofFloat(view,"translationY",0f,moveDis)
}
Direction.LEFT -> {
animation = ObjectAnimator.ofFloat(view,"translationX",0f,-moveDis)
}
Direction.RIGHT -> {
animation = ObjectAnimator.ofFloat(view,"translationX",0f,moveDis)
}
}
animation.start()
animation.addListener(object :Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
animationFinish = false
}
override fun onAnimationEnd(animation: Animator) {
animationFinish = true
//重新设置view的位置
when(dir) {
Direction.UP -> {
view.layout(view.left, view.y.toInt(), view.right, (view.y + gridSize).toInt())
//手动重制
view.translationY = 0F
mFun?.invoke()
}
Direction.DOWN -> {
view.layout(view.left, view.y.toInt(), view.right, (view.y + gridSize).toInt())
view.translationY = 0F
mFun?.invoke()
}
Direction.LEFT -> {
view.layout(view.x.toInt(), view.top, (view.x + gridSize).toInt(), view.bottom)
view.translationX = 0F
mFun?.invoke()
}
Direction.RIGHT -> {
view.layout(view.x.toInt(), view.top, (view.x + gridSize).toInt(), view.bottom)
view.translationX = 0F
mFun?.invoke()
}
}
}
override fun onAnimationCancel(animation: Animator) {
}
override fun onAnimationRepeat(animation: Animator) {
}
})
}
private fun startToLeft() {
logicHelper.setCallback(object :Two028Helper.DoViewLogic {
override fun startDoMoveAndMerge(view: View, move: Int, dir: Direction, removeView: View?, i: Int, j: Int) {
startDoMove(view,move,dir) {
//动画结束时候,消星的情况下,removeView and 更新text内容
val n = logicHelper.entityNumArray[i][j - logicHelper.canMove[i][j]]
(logicHelper.listView[i][j - logicHelper.canMove[i][j]] as EntityNumView).setInt(n)
removeView(removeView) //!!重要,merge的时候一定要removeView,而且要在=null之前,否则丢失引用,再也移除不掉了
}
}
override fun startDoMove(view: View, move: Int, dir: Direction) {
this@TouchView.startDoMove(view,move,dir)
}
})
logicHelper.startToLeft()
}
private fun startToRight() {
logicHelper.setCallback(object :Two028Helper.DoViewLogic {
override fun startDoMoveAndMerge(view: View, move: Int, dir: Direction, removeView: View?, i: Int, j: Int) {
startDoMove(view,move,dir) {
val n = logicHelper.entityNumArray[i][j + logicHelper.canMove[i][j]]
(logicHelper.listView[i][j + logicHelper.canMove[i][j]] as EntityNumView).setInt(n)
removeView(removeView)
}
}
override fun startDoMove(view: View, move: Int, dir: Direction) {
this@TouchView.startDoMove(view,move,dir)
}
})
logicHelper.startToRight()
}
private fun startToTop() {
logicHelper.setCallback(object :Two028Helper.DoViewLogic{
override fun startDoMoveAndMerge(
view: View,
move: Int,
dir: Direction,
removeView: View?,
i: Int,
j: Int
) {
startDoMove(view,move,dir) {
val n = logicHelper.entityNumArray[i-logicHelper.canMove[i][j]][j]
(logicHelper.listView[i-logicHelper.canMove[i][j]][j] as EntityNumView).setInt(n)
removeView(removeView)
}
}
override fun startDoMove(view: View, move: Int, dir: Direction) {
this@TouchView.startDoMove(view,move,dir)
}
})
logicHelper.startToTop()
}
private fun startToBottom() {
logicHelper.setCallback(object :Two028Helper.DoViewLogic {
override fun startDoMoveAndMerge(
view: View,
move: Int,
dir: Direction,
removeView: View?,
i: Int,
j: Int
) {
startDoMove(view,move,dir) {
val n = logicHelper.entityNumArray[i+logicHelper.canMove[i][j]][j]
(logicHelper.listView[i+logicHelper.canMove[i][j]][j] as EntityNumView).setInt(n)
removeView(removeView)
}
}
override fun startDoMove(view: View, move: Int, dir: Direction) {
this@TouchView.startDoMove(view,move,dir)
}
})
logicHelper.startToBottom()
}
init {
Handler(Looper.getMainLooper()).post {
initLayout()
logicHelper.initCanMove()
}
}
class LayoutParams: MarginLayoutParams {
var myMarginStart: Int = 0
var myMarginTop: Int = 0
constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
// 从 XML 属性中解析自定义属性
val a = c.obtainStyledAttributes(attrs, R.styleable.TouchView_Layout)
myMarginStart = a.getDimensionPixelSize(R.styleable.TouchView_Layout_layout_marginStart, 0)
myMarginTop = a.getDimensionPixelSize(R.styleable.TouchView_Layout_layout_marginTop, 0)
a.recycle()
}
constructor(width: Int, height: Int) : super(width, height)
constructor(source: ViewGroup.LayoutParams) : super(source)
constructor(source: MarginLayoutParams) : super(source)
}
/***
* 自定义LayoutParams一定需要重写这三个
*/
override fun generateLayoutParams(attrs: AttributeSet): LayoutParams {
return LayoutParams(context, attrs)
}
override fun generateLayoutParams(p: ViewGroup.LayoutParams): ViewGroup.LayoutParams {
return LayoutParams(p)
}
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
return LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
//底层蒙版画背景
class MainBackView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
): ViewGroup(context,attrs,defStyleAttr) {
private val paint = Paint()
private var linesArray = Array((size+1)*2) { FloatArray(4) }
private var gridSize = 0F
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
gridSize = (this.measuredWidth - 40F - (size+1)*paintStrokeWidth)/size //每一个栅格尺寸
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.setColor(resources.getColor(R.color.player_controller_color))
paint.style = Paint.Style.FILL
paint.strokeWidth = paintStrokeWidth //线条宽度
paint.strokeCap = Paint.Cap.ROUND //末端圆角
computeLinesPoints()
for (i in linesArray.indices) {
canvas.drawLines(linesArray[i],paint)
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// do nothing
}
private fun computeLinesPoints() {
linesArray = Array((size+1)*2) { FloatArray(4) }
//下面代表了第一条横线
linesArray[0][0] = 30F //起点x
linesArray[0][1] = 30F //起点y
linesArray[0][2] = this.measuredWidth - 30F //终点x
linesArray[0][3] = 30F //终点y
//下面代表了第一条竖线
linesArray[size+1][0] = 30F //起点x
linesArray[size+1][1] = 30F //起点y
linesArray[size+1][2] = 30F //终点x
linesArray[size+1][3] = this.measuredHeight - 30F //终点y
//全部横线的坐标点
for (i in 1..size) {
linesArray[i][0] = linesArray[i-1][0]
linesArray[i][1] = linesArray[i-1][1] + gridSize + paintStrokeWidth //20f是线条粗细
linesArray[i][2] = linesArray[i-1][2]
linesArray[i][3] = linesArray[i-1][3] + gridSize + paintStrokeWidth
}
//全部竖线的坐标点
for (i in size+2..linesArray.size-1) {
linesArray[i][0] = linesArray[i-1][0] + gridSize + paintStrokeWidth
linesArray[i][1] = linesArray[i-1][1]
linesArray[i][2] = linesArray[i-1][2] + gridSize + paintStrokeWidth
linesArray[i][3] = linesArray[i-1][3]
}
}
@Deprecated("can not use.if use,you will die")
fun resetLayout(size: Int) {
// this.size = size
invalidate()
}
init {
setBackgroundColor(Color.GRAY)
}
}