• Kotlin高仿微信-第37篇-拍照


      Kotlin高仿微信-项目实践58篇详细讲解了各个功能点,包括:注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。

    Kotlin高仿微信-项目实践58篇,点击查看详情

    效果图:

    实现代码:

    
    
    
    
        
    
        
    
        
    
        
    
        
    
        
    
        
    
        
    
    
    

    /**
     * Author : wangning
     * Email : maoning20080809@163.com
     * Date : 2022/5/23 22:01
     * Description : 拍照
     */
    class CameraFragment : BaseDataBindingFragment(){
    
        override fun getLayoutRes() = R.layout.wc_svideo_camera
    
        private lateinit var outputDirectory: File
        private lateinit var videoCapture: VideoCapture
        private var activeRecording: ActiveRecording? = null
        private lateinit var recordingState: VideoRecordEvent
        private var audioEnabled = true
        private val mainThreadExecutor by lazy { ContextCompat.getMainExecutor(requireContext()) }
        private var isBack = true
        private var imageCapture: ImageCapture? = null
        private lateinit var cameraExecutor: ExecutorService
        private val REQ_CAMREA_CODE = 101
        val EXTENSION_WHITELIST = arrayOf("JPG")
        var enterType = 0
    
        enum class UiState {
            IDLE,       // Not recording, all UI controls are active.
            RECORDING,  // Camera is recording, only display Pause/Resume & Stop button.
            FINALIZED,  // Recording just completes, disable all RECORDING UI controls.
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            TagUtils.d("拍小视频开始。。")
            //initCameraFragment()
            handlePermission()
        }
    
        private fun handlePermission(){
            if(ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
                requestPermissions(arrayOf(Manifest.permission.CAMERA), REQ_CAMREA_CODE)
            } else {
                initCameraFragment()
            }
        }
    
        override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            if(requestCode == REQ_CAMREA_CODE && grantResults != null && grantResults.size > 0){
                if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    initCameraFragment()
                }
            }
        }
    
        override fun onDestroyView() {
            super.onDestroyView()
            cameraExecutor.shutdown()
        }
    
        private fun setGalleryThumbnail(uri: Uri) {
            /*fragmentCameraBinding.btnPhotoView.let { photoViewButton ->
                photoViewButton.post {
                    photoViewButton.setPadding(resources.getDimension(R.dimen.stroke_small).toInt())
                    Glide.with(photoViewButton)
                        .load(uri)
                        .apply(RequestOptions.circleCropTransform())
                        .into(photoViewButton)
                }
            }*/
        }
    
        private suspend fun bindCameraUseCases() {
            //var degree = previewView.display.rotation
    
            val cameraProvider: ProcessCameraProvider = ProcessCameraProvider.getInstance(requireContext()).await()
            val cameraSelector = if (isBack) CameraSelector.DEFAULT_BACK_CAMERA else CameraSelector.DEFAULT_FRONT_CAMERA
    
            val preview = Preview.Builder()
                .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
                .build()
                .apply { setSurfaceProvider(previewView.surfaceProvider) }
    
            val recorder = Recorder.Builder()
                //.setQualitySelector(QualitySelector.of(QualitySelector.QUALITY_SD))
                .setQualitySelector(QualitySelector.of(QualitySelector.QUALITY_FHD))
                .build()
    
            videoCapture = VideoCapture.withOutput(recorder)
    
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                //.setTargetRotation(ROTATION_90) // 设置旋转角度
                .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
                .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
                .build()
    
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    viewLifecycleOwner,
                    cameraSelector,
                    videoCapture,
                    imageCapture,
                    preview
                )
            } catch (e: Exception) {
                TagUtils.e("Use case binding failed ${e}")
                e.printStackTrace()
                resetUIandState("bindToLifecycle failed: $e")
            }
        }
        var outFile : File? = null
        @SuppressLint("MissingPermission")
        private fun startRecording() {
            outFile = createFile(outputDirectory, FILENAME, VIDEO_EXTENSION)
            TagUtils.i("outFile: $outFile")
            val outputOptions: FileOutputOptions = FileOutputOptions.Builder(outFile!!).build()
            activeRecording = videoCapture.output.prepareRecording(requireActivity(), outputOptions)
                .withEventListener(mainThreadExecutor, captureListener)
                .apply { if (audioEnabled) withAudioEnabled() }
                .start()
    
            TagUtils.i("Recording started")
    
        }
    
        private val captureListener = Consumer { event ->
            if (event !is VideoRecordEvent.Status) recordingState = event
    
            updateUI(event)
    
            if (event is VideoRecordEvent.Finalize) showVideo(event)
        }
    
        private fun takePicture() {
            imageCapture?.let { imageCapture ->
                val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)
                val metadata = ImageCapture.Metadata().apply {
                    //isReversedHorizontal = isBack
                }
                val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
                    .setMetadata(metadata)
                    .build()
    
                imageCapture.takePicture(
                    outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
                        override fun onError(exc: ImageCaptureException) {
                            TagUtils.e("Photo capture failed: ${exc.message}")
                        }
    
                        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                            //val savedUri: Uri = output.savedUri ?: Uri.fromFile(photoFile)
    
                            TagUtils.d( "Photo capture succeeded: $outFile")
                            TagUtils.d( "Photo capture 成功: $photoFile")
    
                            lifecycleScope.launch {
                                findNavController()?.popBackStack()
                                var bundle = bundleOf(CommonUtils.Moments.TYPE_IMAGE_PATH to photoFile,
                                    CommonUtils.Moments.TYPE_NAME to CommonUtils.Moments.TYPE_PICTURE,
                                    TYPE_ENTER to enterType)
                                findNavController().navigate( R.id.action_svideo_play, bundle)
                                TagUtils.d("拍照成功 ${photoFile}")
                            }
                        }
                    })
    
                // We can only change the foreground Drawable using API level 23+ API
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    // Display flash animation to indicate that photo was captured
                    container.postDelayed({
                        container.foreground = ColorDrawable(Color.WHITE)
                        container.postDelayed(
                            { container.foreground = null }, ANIMATION_FAST_MILLIS
                        )
                    }, ANIMATION_SLOW_MILLIS)
                }
            }
        }
    
        private fun initCameraFragment() {
            outputDirectory = getOutputDirectory(requireContext())
            cameraExecutor = Executors.newSingleThreadExecutor()
            initializeUI()
            viewLifecycleOwner.lifecycleScope.launch {
                bindCameraUseCases()
            }
        }
    
        private fun switchCamera() {
            isBack = !isBack
            lifecycleScope.launch {
                bindCameraUseCases()
            }
        }
    
        private fun changeFlashMode() {
            when (imageCapture?.flashMode) {
                ImageCapture.FLASH_MODE_AUTO -> {
                    imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON
                    iv_torch.setImageResource(R.drawable.icon_flash_always_on)
                }
                ImageCapture.FLASH_MODE_ON -> {
                    imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
                    iv_torch.setImageResource(R.drawable.icon_flash_always_off)
                }
                ImageCapture.FLASH_MODE_OFF -> {
                    imageCapture?.flashMode = ImageCapture.FLASH_MODE_AUTO
                    iv_torch.setImageResource(R.drawable.icon_flash_auto)
                }
                else -> Unit
            }
        }
    
        @SuppressLint("ClickableViewAccessibility", "MissingPermission")
        private fun initializeUI() {
    
            enterType = arguments?.getInt(TYPE_ENTER) as Int
    
            lifecycleScope.launch(Dispatchers.IO) {
                outputDirectory.listFiles { file ->
                    EXTENSION_WHITELIST.contains(file.extension.uppercase(Locale.ROOT))
                }?.maxOrNull()?.let {
                    setGalleryThumbnail(Uri.fromFile(it))
                }
            }
    
            btn_switch_camera.setOnClickListener {
                switchCamera()
            }
    
            btn_photo_view.setOnClickListener {
                TagUtils.d("点击相册。。。")
                /*findNavController().navigate(
                    CameraFragmentDirections.actionCameraToGallery(
                        outputDirectory.absolutePath
                    )
                )*/
            }
    
            audio_selection.isChecked = audioEnabled
            audio_selection.setOnClickListener {
                audioEnabled = audio_selection.isChecked
            }
    
            btn_record.setOnLongClickListener(object :
                CircleProgressButtonView.OnLongClickListener {
                override fun onLongClick() {
                    if (!this@CameraFragment::recordingState.isInitialized || recordingState is VideoRecordEvent.Finalize) {
                        startRecording()
                    }
                }
    
                override fun onNoMinRecord(currentTime: Int) = Unit
    
                override fun onRecordFinishedListener() {
                    if (activeRecording == null || recordingState is VideoRecordEvent.Finalize) return
                    val recording = activeRecording
                    if (recording != null) {
                        recording.stop()
                        activeRecording = null
                    }
                }
    
            })
    
            /*btn_record.setOnClickListener(CircleProgressButtonView.OnClickListener {
                takePicture()
            })*/
            btn_record.setOnClickListener(object : CircleProgressButtonView.OnClickListener{
                override fun onClick() {
                    takePicture()
                }
            })
    
            iv_torch.setOnClickListener {
                changeFlashMode()
            }
        }
    
        private fun updateUI(event: VideoRecordEvent) {
            val state = if (event is VideoRecordEvent.Status) recordingState.getName()
            else event.getName()
            TagUtils.i("event.getName(): ${event.getName()}")
            when (event) {
                is VideoRecordEvent.Status -> {
                    // placeholder: we update the UI with new status after this when() block,
                    // nothing needs to do here.
                }
                is VideoRecordEvent.Start -> {
                    showUI(UiState.RECORDING, event.getName())
                }
                is VideoRecordEvent.Finalize -> {
                    showUI(UiState.FINALIZED, event.getName())
                }
                is VideoRecordEvent.Pause -> {
                }
                is VideoRecordEvent.Resume -> {
                }
                else -> {
                    TagUtils.e("Error(Unknown Event) from Recorder")
                    return
                }
            }
    
            val stats = event.recordingStats
            val size = stats.numBytesRecorded / 1000
            val time = java.util.concurrent.TimeUnit.NANOSECONDS.toSeconds(stats.recordedDurationNanos)
            var text = "${state}: recorded ${size}KB, in ${time}second"
            if (event is VideoRecordEvent.Finalize)
                text = "${text}\nFile saved to: ${event.outputResults.outputUri}"
    
            capture_status.text = text
            TagUtils.i("recording event: $text")
        }
    
        private fun showUI(state: UiState, status: String = "idle") {
            TagUtils.i("showUI: UiState: $status")
            when (state) {
                UiState.IDLE -> {
                    btn_switch_camera.visibility = View.VISIBLE
                    audio_selection.visibility = View.VISIBLE
                }
                UiState.RECORDING -> {
                    btn_switch_camera.visibility = View.INVISIBLE
                    audio_selection.visibility = View.INVISIBLE
                }
                UiState.FINALIZED -> {
                }
                else -> {
                    val errorMsg = "Error: showUI($state) is not supported"
                    TagUtils.e(errorMsg)
                    return
                }
            }
            capture_status.text = status
        }
    
        private fun resetUIandState(reason: String) {
            showUI(UiState.IDLE, reason)
            audioEnabled = false
            audio_selection.isChecked = audioEnabled
        }
    
        private fun showVideo(event: VideoRecordEvent) {
            TagUtils.d("0小视频路径:showVideo ")
            if (event !is VideoRecordEvent.Finalize) return
    
            lifecycleScope.launch {
                findNavController()?.popBackStack()
                var bundle = bundleOf(CommonUtils.Moments.TYPE_VIDEO_PATH to outFile,
                    CommonUtils.Moments.TYPE_NAME to CommonUtils.Moments.TYPE_VIDEO,
                    TYPE_ENTER to enterType)
                findNavController().navigate( R.id.action_svideo_play, bundle)
            }
        }
    
        companion object {
            const val DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_16_9
            //val TAG: String = CameraFragment::class.java.simpleName
            private const val FILENAME = "yyyyMMddHHmmss"
            private const val VIDEO_EXTENSION = ".mp4"
            private const val PHOTO_EXTENSION = ".jpg"
    
            private const val IMMERSIVE_FLAG_TIMEOUT = 500L
    
            const val ANIMATION_FAST_MILLIS = 50L
            const val ANIMATION_SLOW_MILLIS = 100L
    
            //聊天页面小视频
            const val TYPE_CHAT = 1
            //朋友圈小视频
            const val TYPE_MOMENT = 2
            //进入类型
            const val TYPE_ENTER = "type_enter"
            //返回类型
            const val TYPE_BACK = "type_back"
    
            fun getOutputDirectory(context: Context): File {
                /*val appContext = context.applicationContext
                val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
                    File(it, "SVideo").apply { mkdirs() }
                }
                return if (mediaDir != null && mediaDir.exists())
                    mediaDir else appContext.filesDir*/
                return File(FileUtils.getFilePath())
            }
    
            fun createFile(baseFolder: File, format: String, extension: String) =
                File(baseFolder, SimpleDateFormat(format, Locale.US).format(System.currentTimeMillis()) + extension)
        }
    }
    
    fun VideoRecordEvent.getName(): String {
        return when (this) {
            is VideoRecordEvent.Status -> "Status"
            is VideoRecordEvent.Start -> "Started"
            is VideoRecordEvent.Finalize -> "Finalized"
            is VideoRecordEvent.Pause -> "Paused"
            is VideoRecordEvent.Resume -> "Resumed"
            else -> "Error(Unknown)"
        }
    }

    /**
     * Author : wangning
     * Email : maoning20080809@163.com
     * Date : 2022/5/23 22:05
     * Description : 录制视频
     */
    class CircleProgressButtonView : View {
    
        constructor(context: Context) : this(context, null)
        constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
        constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) {
            init(context, attributeSet)
        }
    
    
        private val WHAT_LONG_CLICK = 1
        private var mBigCirclePaint: Paint? = null
        private var mSmallCirclePaint: Paint? = null
        private var mProgressCirclePaint: Paint? = null
        private var mHeight //当前View的高
                = 0
        private var mWidth //当前View的宽
                = 0
        private var mInitBitRadius = 0f
        private var mInitSmallRadius = 0f
        private var mBigRadius = 0f
        private var mSmallRadius = 0f
        private var mStartTime: Long = 0
        private var mEndTime: Long = 0
        private var isRecording //录制状态
                = false
        private var isMaxTime //达到最大录制时间
                = false
        private var mCurrentProgress //当前进度
                = 0f
    
        private val mLongClickTime: Long = 500 //长按最短时间(毫秒),
    
        private var mTime = 15 //录制最大时间s
    
        private var mMinTime = 3 //录制最短时间
    
        private var mProgressColor //进度条颜色
                = 0
        private var mProgressW = 18f //圆环宽度
    
        //当前手指处于按压状态
        private var isPressed2 = false
        //圆弧进度变化
        private var mProgressAni : ValueAnimator? = null
    
    
        private fun init(context: Context, attrs: AttributeSet?) {
            val a = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressButtonView)
            mMinTime = a.getInt(R.styleable.CircleProgressButtonView_minTime, 0)
            mTime = a.getInt(R.styleable.CircleProgressButtonView_maxTime, 10)
            mProgressW = a.getDimension(R.styleable.CircleProgressButtonView_progressWidth, 12f)
            mProgressColor = a.getColor(
                R.styleable.CircleProgressButtonView_progressColor,
                Color.parseColor("#6ABF66")
            )
            a.recycle()
            initPaint()
            mProgressAni = ValueAnimator.ofFloat(0f, 360f)
            mProgressAni?.setDuration((mTime * 1000).toLong())
        }
    
        private fun initPaint() {
            //初始画笔抗锯齿、颜色
            mBigCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
            mBigCirclePaint!!.color = Color.parseColor("#DDDDDD")
            mSmallCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
            mSmallCirclePaint!!.color = Color.parseColor("#FFFFFF")
            mProgressCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
            mProgressCirclePaint!!.color = mProgressColor
        }
    
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            mWidth = MeasureSpec.getSize(widthMeasureSpec)
            mHeight = MeasureSpec.getSize(heightMeasureSpec)
            mBigRadius = mWidth / 2f * 0.75f
            mInitBitRadius = mBigRadius
            mSmallRadius = mBigRadius * 0.75f
            mInitSmallRadius = mSmallRadius
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            //绘制外圆
            canvas.drawCircle(mWidth / 2f, mHeight / 2f, mBigRadius, mBigCirclePaint!!)
            //绘制内圆
            canvas.drawCircle(mWidth / 2f, mHeight / 2f, mSmallRadius, mSmallCirclePaint!!)
            //录制的过程中绘制进度条
            if (isRecording) drawProgress(canvas)
        }
    
        private fun drawProgress(canvas: Canvas) {
            mProgressCirclePaint!!.strokeWidth = mProgressW
            mProgressCirclePaint!!.style = Paint.Style.STROKE
            //用于定义的圆弧的形状和大小的界限
            val oval = RectF(
                mWidth / 2f - (mBigRadius - mProgressW / 2),
                mHeight / 2f - (mBigRadius - mProgressW / 2),
                mWidth / 2f + (mBigRadius - mProgressW / 2),
                mHeight / 2f + (mBigRadius - mProgressW / 2)
            )
            //根据进度画圆弧
            canvas.drawArc(oval, -90f, mCurrentProgress, false, mProgressCirclePaint!!)
        }
    
        private val mHandler: Handler = object : Handler(Looper.getMainLooper()) {
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                when (msg.what) {
                    WHAT_LONG_CLICK -> {
                        //长按事件触发
                        onLongClickListener2?.onLongClick()
                        //内外圆动画,内圆缩小,外圆放大
                        startAnimation(
                            mBigRadius,
                            mBigRadius * 1.33f,
                            mSmallRadius,
                            mSmallRadius * 0.7f
                        )
                    }
                }
            }
        }
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    isPressed2 = true
                    mStartTime = System.currentTimeMillis()
                    val mMessage = Message.obtain()
                    mMessage.what = WHAT_LONG_CLICK
                    mHandler.sendMessageDelayed(mMessage, mLongClickTime)
                }
                MotionEvent.ACTION_UP -> {
                    isPressed2 = false
                    isRecording = false
                    mEndTime = System.currentTimeMillis()
                    if (mEndTime - mStartTime < mLongClickTime) {
                        mHandler.removeMessages(WHAT_LONG_CLICK)
                        onClickListener2?.onClick()
                    } else {
                        startAnimation(
                            mBigRadius,
                            mInitBitRadius,
                            mSmallRadius,
                            mInitSmallRadius
                        ) //手指离开时动画复原
                        if (mProgressAni != null && mProgressAni!!.currentPlayTime / 1000 < mMinTime && !isMaxTime) {
                            onLongClickListener2?.onNoMinRecord(mMinTime)
                            mProgressAni!!.cancel()
                        } else {
                            //录制完成
                            if (onLongClickListener2 != null && !isMaxTime) {
                                onLongClickListener2?.onRecordFinishedListener()
                            }
                        }
                    }
                }
            }
            return true
        }
    
        private fun startAnimation(bigStart: Float, bigEnd: Float, smallStart: Float, smallEnd: Float) {
            val bigObjAni = ValueAnimator.ofFloat(bigStart, bigEnd)
            bigObjAni.duration = 150
            bigObjAni.addUpdateListener { animation: ValueAnimator ->
                mBigRadius = animation.animatedValue as Float
                invalidate()
            }
            val smallObjAni = ValueAnimator.ofFloat(smallStart, smallEnd)
            smallObjAni.duration = 150
            smallObjAni.addUpdateListener { animation: ValueAnimator ->
                mSmallRadius = animation.animatedValue as Float
                invalidate()
            }
            bigObjAni.start()
            smallObjAni.start()
            smallObjAni.addListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(animation: Animator) {
                    isRecording = false
                }
    
                override fun onAnimationEnd(animation: Animator) {
                    //开始绘制圆形进度
                    if (isPressed2) {
                        isRecording = true
                        isMaxTime = false
                        startProgressAnimation()
                    }
                }
    
                override fun onAnimationCancel(animation: Animator) {}
                override fun onAnimationRepeat(animation: Animator) {}
            })
        }
    
        private fun startProgressAnimation() {
            mProgressAni!!.start()
            mProgressAni!!.addUpdateListener { animation: ValueAnimator ->
                mCurrentProgress = animation.animatedValue as Float
                invalidate()
            }
            mProgressAni!!.addListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(animation: Animator) {}
                override fun onAnimationEnd(animation: Animator) {
                    //录制动画结束时,即为录制全部完成
                    if (onLongClickListener2 != null && isPressed2) {
                        isPressed2 = false
                        isMaxTime = true
                        onLongClickListener2?.onRecordFinishedListener()
                        startAnimation(mBigRadius, mInitBitRadius, mSmallRadius, mInitSmallRadius)
                        //影藏进度进度条
                        mCurrentProgress = 0f
                        invalidate()
                    }
                }
    
                override fun onAnimationCancel(animation: Animator) {}
                override fun onAnimationRepeat(animation: Animator) {}
            })
        }
    
        interface OnLongClickListener {
            fun onLongClick()
    
            //未达到最小录制时间
            fun onNoMinRecord(currentTime: Int)
    
            //录制完成
            fun onRecordFinishedListener()
        }
    
        var onLongClickListener2: OnLongClickListener? = null
    
        fun setOnLongClickListener(onLongClickListener: OnLongClickListener?) {
            this.onLongClickListener2 = onLongClickListener
        }
    
        interface OnClickListener {
            fun onClick()
        }
    
        var onClickListener2: OnClickListener? = null
    
        fun setOnClickListener(onClickListener: OnClickListener) {
            this.onClickListener2 = onClickListener
        }
    
    }

  • 相关阅读:
    web网页设计期末课程大作业__公司官网 (曼谷科技 1页)带psd文件
    龙蜥及其理事分获“2022 OSCAR 尖峰开源社区及项目、尖峰开源人物”奖项
    从0开始学习数据结构 C语言实现 1.前篇及二分查找算法
    我把Github上最牛b的Java教程和实战项目整合成了一个PDF文档
    antd 表单校验问题记录&解决方案
    【学习笔记】RabbitMQ-6 消息的可靠性投递2
    【逻辑与计算机设计】数码系统和数字系统 | Digital systems and number systems
    【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(运行/调试篇)
    SystemServer是如何启动AMS的
    Checking out and building Chromium on Linux
  • 原文地址:https://blog.csdn.net/maoning20080808/article/details/128132661