欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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 {...

效果如下:

Android自定义view之潜艇大作战
根目录 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

相关标签: Android知识