BoatView

自定义View中完成以下几个事情

  • 通过两个资源定时切换,实现探照灯闪烁的效果
  • 通过OverScroller让移动过程更加顺滑
  • 通过一个Rotation Animation,让潜艇在移动时可以调转角度,更加灵动

internal class BoatView(context: Context?) : AppCompatImageView(context) {

private val _scroller by lazy { OverScroller(context) }

private val _res = arrayOf(
R.mipmap.boat_000,
R.mipmap.boat_002
)

private var _rotationAnimator: ObjectAnimator? = null

private var _cnt = 0
set(value) {
field = if (value > 1) 0 else value
}

init {
scaleType = ScaleType.FIT_CENTER
_startFlashing()
}

private fun _startFlashing() {
postDelayed({
setImageResource(_res[_cnt++])
_startFlashing()
}, 500)
}

override fun computeScroll() {
super.computeScroll()

if (_scroller.computeScrollOffset()) {

x = _scroller.currX.toFloat()
y = _scroller.currY.toFloat()

// Keep on drawing until the animation has finished.
postInvalidateOnAnimation()
}

}

/**
* 移动更加顺换
*/
internal fun smoothMoveTo(x: Int, y: Int) {
if (!_scroller.isFinished) _scroller.abortAnimation()
_rotationAnimator?.let { if (it.isRunning) it.cancel() }

val curX = this.x.toInt()
val curY = this.y.toInt()

val dx = (x - curX)
val dy = (y - curY)
_scroller.startScroll(curX, curY, dx, dy, 250)

_rotationAnimator = ObjectAnimator.ofFloat(
this,
“rotation”,
rotation,
Math.toDegrees(atan((dy / 100.toDouble()))).toFloat()
).apply {
duration = 100
start()
}

postInvalidateOnAnimation()
}
}

ForegroundView

  • 通过boat成员持有潜艇对象,并对其进行控制
  • 实现CameraHelper.FaceDetectListener根据人脸识别的回调,移动潜艇到指定位置
  • 游戏开始时,创建潜艇并做开场动画

/**
* 游戏开始时通过动画进入
*/
@MainThread
fun start() {
_isStop = false
if (boat == null) {
boat = Boat(context).also {
post {
addView(it.view, _width, _width)
AnimatorSet().apply {
play(
ObjectAnimator.ofFloat(
it.view,
“y”,
0F,
this@ForegroundView.height / 2f
)
).with(
ObjectAnimator.ofFloat(it.view, “rotation”, 0F, 360F)
)
doOnEnd { _ -> it.view.rotation = 0F }
duration = 1000
}.start()
}
}
}
}

开场动画

游戏开始时,将潜艇通过动画移动到起始位置,即y轴的二分之一处

/**
* 游戏开始时通过动画进入
*/
@MainThread
fun start() {
_isStop = false
if (boat == null) {
boat = Boat(context).also {
post {
addView(it.view, _width, _width)
AnimatorSet().apply {
play(
ObjectAnimator.ofFloat(
it.view,
“y”,
0F,
this@ForegroundView.height / 2f
)
).with(
ObjectAnimator.ofFloat(it.view, “rotation”, 0F, 360F)
)
doOnEnd { _ -> it.view.rotation = 0F }
duration = 1000
}.start()
}
}
}
}

4、相机(Camera)

相机部分主要有TextureView和CameraHelper组成。TextureView提供给Camera承载preview;工具类CameraHelper主要完成以下功能:

  • 开启相机:通过CameraManger打开摄像头
  • 摄像头切换:切换前后置摄像头,
  • 预览:获取Camera提供的可预览尺寸,并适配TextureView显示
  • 人脸识别:检测人脸位置,进行TestureView上的坐标变换

适配PreviewSize

相机硬件提供的可预览尺寸与屏幕实际尺寸(即TextureView尺寸)可能不一致,所以需要在相机初始化时,选取最合适的PreviewSize,避免TextureView上发生画面拉伸等异常

class CameraHelper(val mActivity: Activity, private val mTextureView: TextureView) {

private lateinit var mCameraManager: CameraManager
private var mCameraDevice: CameraDevice? = null
private var mCameraCaptureSession: CameraCaptureSession? = null

private var canExchangeCamera = false                                               //是否可以切换摄像头
private var mFaceDetectMatrix = Matrix()                                            //人脸检测坐标转换矩阵
private var mFacesRect = ArrayList()                                         //保存人脸坐标信息
private var mFaceDetectListener: FaceDetectListener? = null                         //人脸检测回调
private lateinit var mPreviewSize: Size

/**
* 初始化
*/
private fun initCameraInfo() {
mCameraManager = mActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIdList = mCameraManager.cameraIdList
if (cameraIdList.isEmpty()) {
mActivity.toast(“没有可用相机”)
return
}

//获取摄像头方向
mCameraSensorOrientation =
mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
//获取StreamConfigurationMap,它是管理摄像头支持的所有输出格式和尺寸
val configurationMap =
mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!

val previewSize = configurationMap.getOutputSizes(SurfaceTexture::class.java) //预览尺寸

// 当屏幕为垂直的时候需要把宽高值进行调换,保证宽大于高
mPreviewSize = getBestSize(
mTextureView.height,
mTextureView.width,
previewSize.toList()
)

//根据preview的size设置TextureView
mTextureView.surfaceTexture.setDefaultBufferSize(mPreviewSize.width, mPreviewSize.height)
mTextureView.setAspectRatio(mPreviewSize.height, mPreviewSize.width)
}

选取preview尺寸的原则与TextureView的长宽比尽量一致,且面积尽量接近。

initFaceDetect()用来进行人脸的Matrix初始化,后文介绍。

人脸识别

为相机预览,创建一个CameraCaptureSession对象,会话通过CameraCaptureSession.CaptureCallback返回TotalCaptureResult,通过参数可以让其中包括人脸识别的相关信息

/**
* 创建预览会话
*/
private fun createCaptureSession(cameraDevice: CameraDevice) {

// 为相机预览,创建一个CameraCaptureSession对象
cameraDevice.createCaptureSession(
arrayListOf(surface),
object : CameraCaptureSession.StateCallback() {

override fun onConfigured(session: CameraCaptureSession) {
mCameraCaptureSession = session
session.setRepeatingRequest(
captureRequestBuilder.build(),
mCaptureCallBack,
mCameraHandler
)
}

},
mCameraHandler
)
}

private val mCaptureCallBack = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
if (mFaceDetectMode != CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF)
handleFaces(result)

}
}

通过mFaceDetectMatrix对人脸信息进行矩阵变化,确定人脸坐标以使其准确应用到TextureView。

/**
* 处理人脸信息
*/
private fun handleFaces(result: TotalCaptureResult) {
val faces = result.get(CaptureResult.STATISTICS_FACES)!!
mFacesRect.clear()

for (face in faces) {
val bounds = face.bounds

val left = bounds.left
val top = bounds.top
val right = bounds.right
val bottom = bounds.bottom

val rawFaceRect =
RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
mFaceDetectMatrix.mapRect(rawFaceRect)

var resultFaceRect = if (mCameraFacing == CaptureRequest.LENS_FACING_FRONT) {
rawFaceRect
} else {
RectF(
rawFaceRect.left,
rawFaceRect.top - mPreviewSize.width,
rawFaceRect.right,
rawFaceRect.bottom - mPreviewSize.width
)
}

mFacesRect.add(resultFaceRect)

}

mActivity.runOnUiThread {
mFaceDetectListener?.onFaceDetect(faces, mFacesRect)
}
}

最后,在UI线程将包含人脸坐标的Rect通过回调传出:

mActivity.runOnUiThread {
mFaceDetectListener?.onFaceDetect(faces, mFacesRect)
}

FaceDetectMatrix

mFaceDetectMatrix是在获取PreviewSize之后创建的

/**
* 初始化人脸检测相关信息
*/
private fun initFaceDetect() {

val faceDetectModes =
mCameraCharacteristics.get(CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES)  //人脸检测的模式

mFaceDetectMode = when {
faceDetectModes!!.contains(CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL) -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
faceDetectModes!!.contains(CaptureRequest.STATISTICS_FACE_DETECT_MODE_SIMPLE) -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
else -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF
}

if (mFaceDetectMode == CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF) {
mActivity.toast(“相机硬件不支持人脸检测”)
return
}

val activeArraySizeRect =
mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! //获取成像区域
val scaledWidth = mPreviewSize.width / activeArraySizeRect.width().toFloat()
val scaledHeight = mPreviewSize.height / activeArraySizeRect.height().toFloat()

val mirror = mCameraFacing == CameraCharacteristics.LENS_FACING_FRONT

mFaceDetectMatrix.setRotate(mCameraSensorOrientation.toFloat())
mFaceDetectMatrix.postScale(if (mirror) -scaledHeight else scaledHeight, scaledWidth)// 注意交换width和height的位置!
mFaceDetectMatrix.postTranslate(
mPreviewSize.height.toFloat(),
mPreviewSize.width.toFloat()
)

}

5、控制类(GameController)

三大视图层组装完毕,最后需要一个总控类,对游戏进行逻辑控制

主要完成以下工作:

  • 控制游戏的开启/停止
  • 计算游戏的当前得分
  • 检测潜艇的碰撞
  • 对外(Activity或者Fragment等)提供游戏状态监听的接口

初始化

游戏开始时进行相机的初始化,创建GameHelper类并建立setFaceDetectListener回调到ForegroundView

class GameController(
private val activity: AppCompatActivity,
private val textureView: AutoFitTextureView,
private val bg: BackgroundView,
private val fg: ForegroundView
) {

private var camera2HelperFace: CameraHelper? = null
/**
* 相机初始化
*/
private fun initCamera() {
cameraHelper ?: run {
cameraHelper = CameraHelper(activity, textureView).apply {
setFaceDetectListener(object : 
CameraHelper.FaceDetectListener {
override fun onFaceDetect(faces: Array, facesRect: ArrayList) {
if (facesRect.isNotEmpty()) {
fg.onFaceDetect(faces, facesRect)
}
}
})
}
}
}

游戏状态

定义GameState,对外提供状态的监听。目前支持三种状态

  • Start:游戏开始
  • Over:游戏结束
  • Score:游戏得分

sealed class GameState(open val score: Long) {
object Start : GameState(0)
data class Over(override val score: Long) : GameState(score)
data class Score(override val score: Long) : GameState(score)
}

可以在stop、start的时候,更新状态

/**
* 游戏状态
*/
private val _state = MutableLiveData()
internal val gameState: LiveData
get() = _state

/**
* 游戏停止
*/
fun stop() {
bg.stop()
fg.stop()
_state.value = GameState.Over(_score)
_score = 0L
}

/**
* 游戏开始
*/
fun start() {
initCamera()
fg.start()
bg.start()
_state.value = GameState.Start
handler.postDelayed({
startScoring()
}, FIRST_APPEAR_DELAY_MILLIS)
}

计算得分

游戏启动时通过startScoring开始计算得分并通过GameState上报。

目前的规则设置很简单,存活时间即游戏得分

/**
* 开始计分
*/
private fun startScoring() {
handler.postDelayed(
{
fg.boat?.run {
bg.barsList.flatMap { listOf(it.up, it.down) }
.forEach { bar ->
if (isCollision(
bar.x, bar.y, bar.w, bar.h,
this.x, this.y, this.w, this.h
)
) {
stop()
return@postDelayed
}
}
}
_score++
_state.value = GameState.Score(_score)
startScoring()
}, 100
)
}

检测碰撞

isCollision根据潜艇和障碍物当前位置,计算是否发生了碰撞,发生碰撞则GameOver

/**
* 碰撞检测
*/
private fun isCollision(
x1: Float,
y1: Float,
w1: Float,
h1: Float,
x2: Float,
y2: Float,
w2: Float,
h2: Float
): Boolean {
if (x1 > x2 + w2 || x1 + w1 < x2 || y1 > y2 + h2 || y1 + h1 < y2) {
return false
}
return true
}

6、Activity

Activity的工作简单:

  • 权限申请:动态申请Camera权限
  • 监听游戏状态:创建GameController,并监听GameState状态

private fun startGame() {
PermissionUtils.checkPermission(this, Runnable {
gameController.start()
gameController.gameState.observe(this, Observer {
when (it) {
is GameState.Start ->
score.text = “DANGER\nAHEAD”
is GameState.Score ->
score.text = "it.score/10fm"isGameState.Over−>AlertDialog.Builder(this).setMessage("游戏结束!成功推进{it.score / 10f} m" is GameState.Over -> AlertDialog.Builder(this) .setMessage("游戏结束!成功推进 it.score / 10f m"is GameState.Over −>AlertDialog.Builder(this).setMessage("游戏结束!成功推进 {it.score / 10f} 米! ")
.setNegativeButton(“结束游戏”) { _: DialogInterface, _: Int ->
finish()
}.setCancelable(false)
.setPositiveButton(“再来一把”) { _: DialogInterface, _: Int ->
gameController.start()
}.show()
}
})
})
}

ame() {
PermissionUtils.checkPermission(this, Runnable {
gameController.start()
gameController.gameState.observe(this, Observer {
when (it) {
is GameState.Start ->
score.text = “DANGER\nAHEAD”
is GameState.Score ->
score.text = "it.score/10fm"isGameState.Over−>AlertDialog.Builder(this).setMessage("游戏结束!成功推进{it.score / 10f} m" is GameState.Over -> AlertDialog.Builder(this) .setMessage("游戏结束!成功推进 it.score / 10f m"is GameState.Over −>AlertDialog.Builder(this).setMessage("游戏结束!成功推进 {it.score / 10f} 米! ")
.setNegativeButton(“结束游戏”) { _: DialogInterface, _: Int ->
finish()
}.setCancelable(false)
.setPositiveButton(“再来一把”) { _: DialogInterface, _: Int ->
gameController.start()
}.show()
}
})
})
}

Android-手撸抖音“潜艇大挑战”,非科班面试之旅相关推荐

  1. JAVA抖音潜艇挑战_Android 实现抖音小游戏潜艇大挑战的思路详解

    <潜水艇大挑战>是抖音上的一款小游戏,以面部识别来驱动潜艇通过障碍物,最近特别火爆,相信很多人都玩过. 一时兴起自己用Android自定义View也撸了一个,发现只要有好的创意,不用高深的 ...

  2. 计算机挑战音乐,抖音平板加速挑战背景歌曲是什么歌?

    抖音平板加速挑战背景音乐是什么歌?最近抖音上有一个很火的挑战,很多明星比如黄晓明邓紫棋这些都在挑战这个活动,据悉,这是因为这次的事件,大家只能在家,不过在家也要锻炼身体呀,下面小编就为玩家带来抖音平板 ...

  3. 用计算机弹抖音上最火的纯音乐,抖音十大最火歌曲排名,包含几首超燃bgm纯音乐...

    最近的这四五年,抖音成为了年轻人生活中不可或缺的一部分,可传自己制作的视频,可晒日常的快乐种种,但这都不稀奇,稀奇的是各种纯音乐作品也成了抖音上的畅谈的对象,只要你去听,会燃起你内心的火热,那今天,小 ...

  4. 10分钟手把手教你用Android手撸一个简易的个人记账App

    用Android手撸一个简易的个人记账系统 ⛱️序言

  5. Android快速集成抖音分享

    现在大火的app,抖音肯定有姓名.19年初抖音就开始支持Android应用接入抖音分享以及登录了,目前市场上我知道的轻颜相机已经集成分享.最近有一个新需求就是集成抖音分享,集成较为简单,但是也由于疏忽 ...

  6. 飞机大作战java源代码_java实现抖音飞机大作战

    本文实例为大家分享了java抖音飞机大作战的具体代码,供大家参考,具体内容如下 Airplane.java package zmf.game.shoot; import java.util.Rando ...

  7. ADB 控制Android自动刷抖音

    ADB 控制Android自动刷抖音 前言 思路 具体实现 手机打开开发者模式 使用ADB连接手机 使用ADB控制手机滑动 使用bat读取设备列表 使用bat判断设备列表中是否有设备 当有设备时开始滑 ...

  8. 计算机音乐抖音神曲,2019抖音十大神曲纯音乐 2019抖音最火的纯音乐盘点

    导语:在各种曲风的音乐中不得不说纯音乐在治愈性上是很强大的,不过曾经比较青睐纯音乐的朋友或许并不是很多,但目前抖音的出现,不少的纯音乐可以说是让不少人是心情好的时候也会循环播放,下面排行榜123网就整 ...

  9. JAVA抖音潜艇挑战,抖音怎么玩潜水艇挑战

    抖音潜水艇挑战怎么玩?最近抖音推出了一个特效,是游戏特效,叫潜水艇挑战,很多抖音玩家都在玩,据说,和之前的FlappyBird一样,时间越久潜水艇不坠毁就算获胜,下面小编就为玩家带来抖音潜水艇挑战玩法 ...

  10. JAVA抖音潜艇挑战_抖音潜水艇挑战怎么玩 抖音潜水艇挑战玩法和潜艇王者称号获得方法...

    抖音潜水艇挑战怎么玩?最近抖音上开始流行潜水艇挑战,大家知道这是怎么玩的吗?这主要是使用抖音上推出的一个叫"潜水艇挑战"的游戏特效,和之前的FlappyBird一样,时间越久潜水艇 ...

最新文章

  1. DIY自己的AI助理,萝莉御姐暖男霸道总裁全凭你定义,微软小冰团队发布新框架...
  2. oracle集群 节点切换不,Oracle 11gR2 RAC集群单节点关闭开启
  3. 回看2020-数据库大讲堂
  4. HDU 1525 Euclid's Game
  5. dede无法在这个位置找到head.html2,织梦搜索:DedeCMS 提示信息
  6. Springmvc集成CXF请看教程二
  7. php斐波那契数列循环,两种php实现斐波那契数列的方法
  8. activiti5 工作流
  9. Python 可轻松获取天气数据、可视化分析
  10. windriver 自动生成pcie驱动
  11. linux gprs模块 sim900芯片 ppp拨号上网
  12. 三年程序员成功转型项目经理
  13. C语言一行一行读取文件
  14. zigbee PRO 事件
  15. JSP水电费管理系统myeclipse开发mysql数据库web结构java编程
  16. 匈牙利命名法Hungarian Notation
  17. Navicat Premium 12卸载注册表 激活码 彻底删除
  18. 挡土墙lisp程序_挡土墙设计程序说明
  19. HTTP代理中的api 代理的使用方法有什么?
  20. 洛谷 P4389 付公主的背包 多项式exp

热门文章

  1. Visio 2013画直线问题总结(折线变直,交叉时产生的交叉桥)
  2. openwrt热插拔HotPlug
  3. 你不是痘痘肌,你只是管不住嘴
  4. [推荐]白纸上的黑点和黑纸上的白点
  5. RFBnet论文及其代码详解
  6. 4-20mA电流光纤中继器的原理和应用
  7. 程序员的奋斗史(三十四)——人在囧途之应聘篇(四)
  8. 【程序设计与实践】实验四:自动寄存柜(C语言)
  9. 快速入门Unity机器学习:三:
  10. 关于蓝牙无线通信与AOA定位技术