Android自定义view之潜艇大作战
程序员文章站
2022-07-08 09:49:55
效果如下:根目录 build.gradle ext.kotlin_version = '1.3.61' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"model的build.gradleapply plugin: 'kotlin-android'apply plugin: 'kotlin-android-extensions'android{ compileOptions {...
效果如下:
根目录 build.gradle
ext.kotlin_version = '1.3.61'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
model的build.gradle
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android{
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
implementation 'androidx.core:core-ktx:1.2.0'
障碍物基类
sealed class Bar(context: Context) {
protected open val bmp = context.getDrawable(R.mipmap.bar)!!.toBitmap()
protected abstract val srcRect: Rect
private lateinit var dstRect: Rect
private val paint = Paint()
var h = 0F
set(value) {
field = value
dstRect = Rect(0, 0, w.toInt(), h.toInt())
}
var w = 0F
set(value) {
field = value
dstRect = Rect(0, 0, w.toInt(), h.toInt())
}
var x = 0F
set(value) {
view.x = value
field = value
}
val y
get() = view.y
internal val view by lazy {
BarView(context) {
it?.apply {
drawBitmap(
bmp,
srcRect,
dstRect,
paint
)
}
}
// .apply {
// setBackgroundColor(context.getColor(R.color.colorAccent))
// }
}
}
/**
* 屏幕上方障碍物
*/
class UpBar(context: Context, container: ViewGroup) : Bar(context) {
private val _srcRect by lazy(LazyThreadSafetyMode.NONE) {
Rect(0, (bmp.height * (1 - (h / container.height))).toInt(), bmp.width, bmp.height)
}
override val srcRect: Rect
get() = _srcRect
}
/**
* 屏幕下方障碍物
*/
class DnBar(context: Context, container: ViewGroup) : Bar(context) {
override val bmp = super.bmp.let {
Bitmap.createBitmap(
it, 0, 0, it.width, it.height,
Matrix().apply { postRotate(-180F) }, true
)
}
private val _srcRect by lazy(LazyThreadSafetyMode.NONE) {
Rect(0, 0, bmp.width, (bmp.height * (h / container.height)).toInt())
}
override val srcRect: Rect
get() = _srcRect
}
@SuppressLint("ViewConstructor")
internal class BarView(context: Context?, private val block: (Canvas?) -> Unit) :
View(context) {
override fun onDraw(canvas: Canvas?) {
block((canvas))
}
}
后景容器类
class BackgroundView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
private lateinit var _timer: Timer
private var _random = Random()
private val _anims = mutableListOf<ValueAnimator>()
private val _barWidth by lazy { width * BAR_WIDTH_FACTOR } // 障碍物默认宽度
private val _barHeight by lazy { height * BAR_HEIGHT_FACTOR } // 障碍物默认高度
private val _gap by lazy { height * BAR_GAP_FACTOR } // 障碍物空洞间隙
private val _step by lazy { height * 0.1F } // 高度调整步进单位
internal val barsList = mutableListOf<Bars>()
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
barsList.flatMap { listOf(it.up, it.down) }.forEach {
val w = it.view.measuredWidth
val h = it.view.measuredHeight
when (it) {
is UpBar -> it.view.layout(0, 0, w, h)
else -> it.view.layout(0, height - h, w, height)
}
}
}
/**
* 游戏结束,停止所有障碍物的移动
*/
@UiThread
fun stop() {
_timer.cancel()
_anims.forEach { it.cancel() }
_anims.clear()
}
/**
* 定时刷新障碍物:
* 1. 创建
* 2. 添加到视图
* 3. 移动
*/
@UiThread
fun start() {
_clearBars()
Timer().also { _timer = it }.schedule(object : TimerTask() {
override fun run() {
post {
_createBars(context, barsList.lastOrNull()).let {
_addBars(it)
_moveBars(it)
}
}
}
},
FIRST_APPEAR_DELAY_MILLIS,
BAR_APPEAR_INTERVAL_MILLIS
)
}
/**
* 创建障碍物(上下两个为一组)
*/
private fun _createBars(context: Context, pre: Bars?) = run {
val up = UpBar(context, this).apply {
h = pre?.let {
val step = when {
it.up.h >= height - _gap - _step -> -_step
it.up.h <= _step -> _step
_random.nextBoolean() -> _step
else -> -_step
}
it.up.h + step
} ?: _barHeight
w = _barWidth
}
val down = DnBar(context, this).apply {
h = height - up.h - _gap
w = _barWidth
}
Bars(up, down)
}
/**
* 添加到屏幕
*/
private fun _addBars(bars: Bars) {
barsList.add(bars)
bars.asArray().forEach {
addView(
it.view,
ViewGroup.LayoutParams(
it.w.toInt(),
it.h.toInt()
)
)
}
}
/**
* 使用属性动画移动障碍物(从屏幕左侧到右侧)
*/
private fun _moveBars(bars: Bars) {
_anims.add(
ValueAnimator.ofFloat(width.toFloat(), -_barWidth)
.apply {
addUpdateListener {
bars.asArray().forEach { bar ->
bar.x = it.animatedValue as Float
if (bar.x + bar.w <= 0) {
post { removeView(bar.view) }
}
}
}
duration = BAR_MOVE_DURATION_MILLIS
interpolator = LinearInterpolator()
start()
})
}
/**
* 游戏重启时,清空障碍物
*/
private fun _clearBars() {
barsList.clear()
removeAllViews()
}
}
internal data class Bars(val up: Bar, val down: Bar)
private fun Bars.asArray() = arrayOf(up, down)
private const val BAR_WIDTH_FACTOR = 0.22F
private const val BAR_HEIGHT_FACTOR = 0.35F
private const val BAR_GAP_FACTOR = BAR_HEIGHT_FACTOR
private const val BAR_MOVE_DURATION_MILLIS = 4000L
private const val BAR_APPEAR_INTERVAL_MILLIS = (BAR_MOVE_DURATION_MILLIS / 2.2).toLong()
internal const val FIRST_APPEAR_DELAY_MILLIS = 3000L
潜艇类
class Boat(context: Context) {
internal val view by lazy { BoatView(context) }
val h
get() = view.height.toFloat()
val w
get() = view.width.toFloat()
val x
get() = view.x
val y
get() = view.y
/**
* 移动到指定坐标
*/
fun moveTo(x: Int, y: Int) {
view.smoothMoveTo(x, y)
}
}
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()
}
}
前景容器类
class ForegroundView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs),
CameraHelper.FaceDetectListener {
private var _isStop: Boolean = false
internal var boat: Boat? = null
private val _width by lazy { (width * BOAT_WIDTH_FACTOR).toInt() }
private val _widthOffset by lazy { dip2px(context, 50F) }
private val _heightOffset by lazy { dip2px(context, 150F) }
/**
* 游戏停止,潜艇不再移动
*/
@MainThread
fun stop() {
_isStop = true
}
/**
* 游戏开始时通过动画进入
*/
@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()
}
}
}
}
/**
* 接受人脸识别的回调,移动位置
*/
override fun onFaceDetect(faces: Array<Face>, facesRect: ArrayList<RectF>) {
if (_isStop) return
if (facesRect.isNotEmpty()) {
boat?.run {
val face = facesRect.first()
val x = (face.left - _widthOffset).toInt()
val y = (face.top + _heightOffset).toInt()
moveTo(x, y)
}
_face = facesRect.first()
}
}
//debug
private val _paint by lazy {
Paint().apply {
color = Color.parseColor("#42ed45")
style = Paint.Style.STROKE
strokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1f,
context.resources.displayMetrics
)
isAntiAlias = true
}
}
private var _face: RectF? = null
set(value) {
field = value
invalidate()
}
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
_face?.let {
canvas.drawRect(it, _paint)
}
}
}
private const val BOAT_WIDTH_FACTOR = 0.2F
private fun dip2px(context: Context, dp: Float): Float {
val scale = context.resources.displayMetrics.density
return dp * scale * 0.5f
}
相机帮助类
fun Context.toast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
class CameraHelper(val mActivity: Activity, private val mTextureView: XKAutoFitTextureView) {
private lateinit var mCameraManager: CameraManager
private var mCameraDevice: CameraDevice? = null
private var mCameraCaptureSession: CameraCaptureSession? = null
private var mCameraId = "0"
private lateinit var mCameraCharacteristics: CameraCharacteristics
private var mCameraSensorOrientation = 0 //摄像头方向
private var mCameraFacing = CameraCharacteristics.LENS_FACING_BACK //默认使用前置摄像头
private var mFaceDetectMode = CaptureResult.STATISTICS_FACE_DETECT_MODE_OFF //人脸检测模式
private var canExchangeCamera = false //是否可以切换摄像头
private var mFaceDetectMatrix = Matrix() //人脸检测坐标转换矩阵
private var mFacesRect = ArrayList<RectF>() //保存人脸坐标信息
private var mFaceDetectListener: FaceDetectListener? = null //人脸检测回调
private var mCameraHandler: Handler
private val handlerThread = HandlerThread("CameraThread")
private lateinit var mPreviewSize: Size
interface FaceDetectListener {
fun onFaceDetect(faces: Array<Face>, facesRect: ArrayList<RectF>)
}
fun setFaceDetectListener(listener: FaceDetectListener) {
this.mFaceDetectListener = listener
}
init {
handlerThread.start()
mCameraHandler = Handler(handlerThread.looper)
mTextureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture?,
width: Int,
height: Int
) {
configureTransform(width, height)
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
releaseCamera()
return true
}
override fun onSurfaceTextureAvailable(
surface: SurfaceTexture?,
width: Int,
height: Int
) {
initCameraInfo()
configureTransform(width, height)
}
}
}
/**
* 初始化
*/
private fun initCameraInfo() {
mCameraManager = mActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIdList = mCameraManager.cameraIdList
if (cameraIdList.isEmpty()) {
mActivity.toast("没有可用相机")
return
}
for (id in cameraIdList) {
val cameraCharacteristics = mCameraManager.getCameraCharacteristics(id)
val facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == mCameraFacing) {
mCameraId = id
mCameraCharacteristics = cameraCharacteristics
}
}
val supportLevel =
mCameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
if (supportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
mActivity.toast("相机硬件不支持新特性")
}
//获取摄像头方向
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()
)
mTextureView.surfaceTexture.setDefaultBufferSize(mPreviewSize.width, mPreviewSize.height)
mTextureView.setAspectRatio(mPreviewSize.height, mPreviewSize.width)
initFaceDetect()
openCamera()
}
private fun getBestSize(
targetWidth: Int,
targetHeight: Int,
sizeList: List<Size>
): Size {
val bigEnough = ArrayList<Size>() //比指定宽高大的Size列表
val notBigEnough = ArrayList<Size>() //比指定宽高小的Size列表
for (size in sizeList) {
//宽高比 == 目标值宽高比
if (size.width == size.height * targetWidth / targetHeight
) {
if (size.width >= targetWidth && size.height >= targetHeight)
bigEnough.add(size)
else
notBigEnough.add(size)
}
}
//选择bigEnough中最小的值 或 notBigEnough中最大的值
return when {
bigEnough.size > 0 -> Collections.min(bigEnough, CompareSizesByArea())
notBigEnough.size > 0 -> Collections.max(notBigEnough, CompareSizesByArea())
else -> sizeList[0]
}
}
/**
* 初始化人脸检测相关信息
*/
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()
)
}
/**
* 打开相机
*/
@SuppressLint("MissingPermission")
private fun openCamera() {
mCameraManager.openCamera(mCameraId, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
mCameraDevice = camera
createCaptureSession(camera)
}
override fun onDisconnected(camera: CameraDevice) {
}
override fun onError(camera: CameraDevice, error: Int) {
mActivity.toast("打开相机失败!$error")
}
}, mCameraHandler)
}
/**
* 创建预览会话
*/
private fun createCaptureSession(cameraDevice: CameraDevice) {
val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val surface = Surface(mTextureView.surfaceTexture)
captureRequestBuilder.addTarget(surface) // 将CaptureRequest的构建器与Surface对象绑定在一起
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
) // 闪光灯
captureRequestBuilder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
) // 自动对焦
if (mFaceDetectMode != CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF)
captureRequestBuilder.set(
CaptureRequest.STATISTICS_FACE_DETECT_MODE,
CameraCharacteristics.STATISTICS_FACE_DETECT_MODE_SIMPLE
)//人脸检测
// 为相机预览,创建一个CameraCaptureSession对象
cameraDevice.createCaptureSession(
arrayListOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigureFailed(session: CameraCaptureSession) {
mActivity.toast("开启预览会话失败!")
}
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)
canExchangeCamera = true
}
override fun onCaptureFailed(
session: CameraCaptureSession,
request: CaptureRequest,
failure: CaptureFailure
) {
super.onCaptureFailed(session, request, failure)
mActivity.toast("开启预览失败!")
}
}
/**
* 处理人脸信息
*/
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)
}
}
/**
* 切换摄像头
*/
fun exchangeCamera() {
if (mCameraDevice == null || !canExchangeCamera || !mTextureView.isAvailable) return
mCameraFacing = if (mCameraFacing == CameraCharacteristics.LENS_FACING_FRONT)
CameraCharacteristics.LENS_FACING_BACK
else
CameraCharacteristics.LENS_FACING_FRONT
releaseCamera()
initCameraInfo()
}
fun releaseCamera() {
mCameraCaptureSession?.close()
mCameraCaptureSession = null
mCameraDevice?.close()
mCameraDevice = null
canExchangeCamera = false
}
fun releaseThread() {
handlerThread.quitSafely()
handlerThread.join()
}
/**
* Configures the necessary [android.graphics.Matrix] transformation to `mTextureView`.
* This method should be called after the camera preview size is determined in
* setUpCameraOutputs and also the size of `mTextureView` is fixed.
*
* @param viewWidth The width of `mTextureView`
* @param viewHeight The height of `mTextureView`
*/
private fun configureTransform(viewWidth: Int, viewHeight: Int) {
val rotation = mActivity.windowManager.defaultDisplay.rotation
val matrix = Matrix()
val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
val bufferRect = RectF(0f, 0f, mPreviewSize.height.toFloat(), mPreviewSize.width.toFloat())
val centerX = viewRect.centerX()
val centerY = viewRect.centerY()
if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
val scale = Math.max(
viewHeight.toFloat() / mPreviewSize.height,
viewWidth.toFloat() / mPreviewSize.width
)
matrix.postScale(scale, scale, centerX, centerY)
matrix.postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY)
} else if (Surface.ROTATION_180 == rotation) {
matrix.postRotate(180f, centerX, centerY)
}
mTextureView.setTransform(matrix)
}
private class CompareSizesByArea : Comparator<Size> {
override fun compare(size1: Size, size2: Size): Int {
return java.lang.Long.signum(size1.width.toLong() * size1.height - size2.width.toLong() * size2.height)
}
}
}
调整到指定的高宽比
public class XKAutoFitTextureView extends TextureView {
private int mRatioWidth =0;
private int mRatioHeight =0;
public XKAutoFitTextureView(Context context) {
this(context,null);
}
public XKAutoFitTextureView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public XKAutoFitTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setAspectRatio(int width,int height){
if(width <0 || height <0){
throw new IllegalArgumentException("Size cannot be negative.");
}
mRatioWidth =width;
mRatioHeight =height;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if(0 == mRatioHeight || 0==mRatioWidth){
setMeasuredDimension(width,height);
}else if (width == height * mRatioWidth/mRatioHeight){
setMeasuredDimension(width,height);
}else if (width<height * mRatioWidth / mRatioHeight){
setMeasuredDimension(height * mRatioWidth/mRatioHeight,height);
}else {
setMeasuredDimension(width,width*mRatioHeight/mRatioWidth);
}
}
}
游戏控制类
class GameController(
private val activity: AppCompatActivity,
private val textureView: XKAutoFitTextureView,
private val bg: BackgroundView,
private val fg: ForegroundView
) {
init {
activity.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
cameraHelper?.releaseCamera()
cameraHelper?.releaseThread()
}
})
}
private val handler by lazy {
android.os.Handler(activity.mainLooper)
}
private var cameraHelper: CameraHelper? = null
private var _score: Long = 0L
/**
* 游戏停止
*/
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)
}
/**
* 开始计分
*/
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
)
}
/**
* 碰撞检测
*/
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
}
/**
* 相机初始化
*/
private fun initCamera() {
cameraHelper ?: run {
cameraHelper = CameraHelper(activity, textureView).apply {
setFaceDetectListener(object : CameraHelper.FaceDetectListener {
override fun onFaceDetect(faces: Array<Face>, facesRect: ArrayList<RectF>) {
if (facesRect.isNotEmpty()) {
fg.onFaceDetect(faces, facesRect)
}
}
})
}
}
}
/**
* 切换摄像头
*/
fun switchCamera() {
cameraHelper?.exchangeCamera()
}
/**
* 游戏状态
*/
private val _state = MutableLiveData<GameState>()
internal val gameState: LiveData<GameState>
get() = _state
}
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)
}
权限管理类
object PermissionUtils {
const val PERMISSION_REQUEST_CODE = 100
const val REQUEST_PERMISSION = Manifest.permission.CAMERA
const val PERMISSION_SETTING_CODE = 101
private var permissionExplainDialog: AlertDialog? = null
private var permissionSettingDialog: AlertDialog? = null
fun checkPermission(
activity: AppCompatActivity,
callBack: Runnable
) {
if (ContextCompat.checkSelfPermission(
activity,
REQUEST_PERMISSION
) == PackageManager.PERMISSION_GRANTED
) callBack.run()
else startRequestPermission(activity)
}
/**
* 如果用户之前拒绝过,展示需要权限的提示框,否则直接请求相关权限
*/
private fun startRequestPermission(activity: AppCompatActivity) {
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity,
REQUEST_PERMISSION
)
) {
showPermissionExplainDialog(activity)
} else {
requestPermission(activity)
}
}
private fun requestPermission(activity: AppCompatActivity) {
ActivityCompat.requestPermissions(
activity,
arrayOf(REQUEST_PERMISSION),
PERMISSION_REQUEST_CODE
)
}
/**
* 展示一个对话框,解释为什么需要此权限
*/
private fun showPermissionExplainDialog(
activity: AppCompatActivity
) {
if (permissionExplainDialog == null) {
permissionExplainDialog = AlertDialog.Builder(activity).setTitle("权限申请")
.setMessage(
"您刚才拒绝了相关权限,开始游戏需要重新申请权限"
)
.setPositiveButton("申请权限") { dialog, _ ->
requestPermission(activity)
dialog.cancel()
}
.setNegativeButton("退出游戏") { dialog, _ ->
dialog.cancel()
activity.finish()
}
.create()
}
permissionExplainDialog?.let {
if (!it.isShowing) {
it.show()
}
}
}
fun showPermissionSettingDialog(activity: AppCompatActivity) {
if (permissionSettingDialog == null) {
permissionSettingDialog = AlertDialog.Builder(activity)
.setTitle("权限设置")
.setMessage("您刚才拒绝了相关的权限,请到应用设置页面更改应用的权限")
.setPositiveButton("确定") { dialog, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
val uri = Uri.fromParts("package", activity.packageName, null)
intent.data = uri
activity.startActivityForResult(intent, PERMISSION_SETTING_CODE)
dialog.cancel()
}
.setNegativeButton("取消") { dialog, _ ->
dialog.cancel()
activity.finish()
}
.create()
}
permissionSettingDialog?.let {
if (!it.isShowing) {
it.show()
}
}
}
}
activity_two.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/root"
android:layout_height="match_parent"
tools:context=".GameActivity">
<TextView
android:id="@+id/score"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:keepScreenOn="true"
android:textColor="#33b5e5"
android:textSize="50sp"
android:textStyle="bold" />
<com.xiaokexin.testvideodemo.game.BackgroundView
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.xiaokexin.testvideodemo.game.ForegroundView
android:id="@+id/foreground"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- This FrameLayout insets its children based on system windows using
android:fitsSystemWindows. -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:id="@+id/fullscreen_content_controls"
style="?metaButtonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:background="@color/black_overlay"
android:orientation="horizontal"
tools:ignore="UselessParent">
<Button
android:id="@+id/switch_button"
style="?metaButtonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="切换摄像头" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
TwoActivity
class GameActivity : AppCompatActivity() {
private val gameController by lazy {
XKAutoFitTextureView(this).let {
root.addView(
it,
0,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
GameController(this, it, background, foreground)
}
}
private val mHideHandler = Handler()
private val mHidePart2Runnable = Runnable {
// Delayed removal of status and navigation bar
// Note that some of these constants are new as of API 16 (Jelly Bean)
// and API 19 (KitKat). It is safe to use them, as they are inlined
// at compile-time and do nothing on earlier devices.
root.systemUiVisibility =
View.SYSTEM_UI_FLAG_LOW_PROFILE or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
}
private val mShowPart2Runnable = Runnable {
// Delayed display of UI elements
fullscreen_content_controls.visibility = View.VISIBLE
}
private var mVisible: Boolean = false
private val mHideRunnable = Runnable { hide() }
/**
* Touch listener to use for in-layout UI controls to delay hiding the
* system UI. This is to prevent the jarring behavior of controls going away
* while interacting with activity UI.
*/
private val mDelayHideTouchListener = View.OnTouchListener { _, _ ->
if (AUTO_HIDE) {
delayedHide(AUTO_HIDE_DELAY_MILLIS)
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setStatusBarColor(Color.TRANSPARENT);
setContentView(R.layout.activity_two)
mVisible = true
// Set up the user interaction to manually show or hide the system UI.
root.setOnClickListener { toggle() }
// Upon interacting with UI controls, delay any scheduled hide()
// operations to prevent the jarring behavior of controls going away
// while interacting with the UI.
switch_button.setOnTouchListener { v, event ->
gameController.switchCamera()
mDelayHideTouchListener.onTouch(v, event)
}
mHidePart2Runnable.run()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (permissions[0] == REQUEST_PERMISSION && requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startGame()
} else {
PermissionUtils.showPermissionSettingDialog(this)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
PERMISSION_SETTING_CODE -> {
startGame()
}
}
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// Trigger the initial hide() shortly after the activity has been
// created, to briefly hint to the user that UI controls
// are available.
delayedHide(100)
startGame()
}
@SuppressLint("SetTextI18n")
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 / 10f} m"
is GameState.Over ->
AlertDialog.Builder(this)
.setMessage("游戏结束!成功推进 ${it.score / 10f} 米! ")
.setNegativeButton("结束游戏") { _: DialogInterface, _: Int ->
finish()
}.setCancelable(false)
.setPositiveButton("再来一把") { _: DialogInterface, _: Int ->
gameController.start()
}.show()
}
})
})
}
private fun toggle() {
if (mVisible) {
hide()
} else {
show()
}
}
private fun hide() {
// Hide UI first
fullscreen_content_controls.visibility = View.GONE
mVisible = false
// Schedule a runnable to remove the status and navigation bar after a delay
mHideHandler.removeCallbacks(mShowPart2Runnable)
mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY.toLong())
}
private fun show() {
// Show the system bar
root.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
mVisible = true
// Schedule a runnable to display UI elements after a delay
mHideHandler.removeCallbacks(mHidePart2Runnable)
mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY.toLong())
}
/**
* Schedules a call to hide() in [delayMillis], canceling any
* previously scheduled calls.
*/
private fun delayedHide(delayMillis: Int) {
mHideHandler.removeCallbacks(mHideRunnable)
mHideHandler.postDelayed(mHideRunnable, delayMillis.toLong())
}
companion object {
/**
* Whether or not the system UI should be auto-hidden after
* [AUTO_HIDE_DELAY_MILLIS] milliseconds.
*/
private val AUTO_HIDE = true
/**
* If [AUTO_HIDE] is set, the number of milliseconds to wait after
* user interaction before hiding the system UI.
*/
private val AUTO_HIDE_DELAY_MILLIS = 3000
/**
* Some older devices needs a small delay between UI widget updates
* and a change of the status and navigation bar.
*/
private val UI_ANIMATION_DELAY = 300
}
}
Androidmanifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.permission.camera"/>
感谢观看,记得点赞哟!
本文地址:https://blog.csdn.net/qq_42795723/article/details/107579555