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

实现代码:
/** * 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
}
}