Kotlin高仿微信-项目实践58篇详细讲解了各个功能点,包括:注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。
Kotlin高仿微信-项目实践58篇,点击查看详情
效果图:
实现代码:
<?xml version="1.0" encoding="utf-8"?> <layout> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.camera.view.PreviewView android:id="@+id/previewView" android:layout_width="match_parent" android:layout_height="match_parent" /> <CheckBox android:id="@+id/audio_selection" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" android:layout_marginTop="20dp" android:layout_marginEnd="100dp" android:visibility="gone" android:buttonTint="@android:color/white" android:text="Audio" android:textColor="@android:color/white" /> <ImageButton android:id="@+id/iv_torch" android:layout_width="40dp" android:layout_height="40dp" android:layout_gravity="end" android:layout_marginTop="16dp" android:layout_marginEnd="@dimen/margin_small" android:background="@android:color/transparent" android:src="@drawable/icon_flash_auto" android:visibility="gone" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageButton android:id="@+id/btn_switch_camera" android:layout_width="48dp" android:layout_height="48dp" android:layout_gravity="top|right" android:layout_marginEnd="@dimen/margin_small" android:layout_marginTop="@dimen/margin_small" android:background="@android:color/transparent" android:contentDescription="@string/switch_camera_button_alt" android:padding="@dimen/spacing_small" android:scaleType="fitCenter" app:srcCompat="@drawable/wc_svideo_switch" /> <ImageButton android:id="@+id/btn_back" android:layout_width="@dimen/dp_40" android:layout_height="@dimen/dp_40" android:layout_gravity="bottom" android:layout_marginStart="@dimen/margin_small" android:layout_marginBottom="60dp" android:background="@android:color/transparent" android:contentDescription="@string/switch_camera_button_alt" android:padding="@dimen/spacing_small" android:scaleType="fitCenter" app:srcCompat="@drawable/wc_svideo_camera_back" /> <com.wn.wechatclientdemo.svideo.CircleProgressButtonView android:id="@+id/btn_record" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="bottom|center" android:layout_marginBottom="40dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:maxTime="15" app:progressWidth="8dp" /> <ImageButton android:id="@+id/btn_photo_view" android:layout_width="@dimen/round_button_medium" android:layout_height="@dimen/round_button_medium" android:layout_gravity="end|bottom" android:layout_marginEnd="@dimen/margin_small" android:layout_marginBottom="@dimen/margin_xlarge" android:background="@drawable/wc_svideo_outer_circle" android:contentDescription="@string/gallery_button_alt" android:visibility="gone" android:padding="@dimen/spacing_large" android:scaleType="fitCenter" app:srcCompat="@drawable/wc_svideo_photo" /> <TextView android:id="@+id/capture_status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center" android:layout_marginBottom="20dp" android:lines="2" android:maxLines="2" android:visibility="gone" android:text="@string/Idle" android:textColor="#ff0" /> </FrameLayout> </layout>
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/5/23 22:01 * Description : 拍照 */ class CameraFragment : BaseDataBindingFragment<WcSvideoCameraBinding>(){ override fun getLayoutRes() = R.layout.wc_svideo_camera private lateinit var outputDirectory: File private lateinit var videoCapture: VideoCapture<Recorder> 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<out String>, 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<VideoRecordEvent> { 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 } }