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
}
}









![[附源码]计算机毕业设计在线图书销售系统Springboot程序](https://img-blog.csdnimg.cn/84c96e33bb744adb88373b0b0f6dab25.png)






![[激光原理与应用-28]:《激光原理与技术》-14- 激光产生技术 - 激光的主要参数与指标](https://img-blog.csdnimg.cn/img_convert/9b0726ec89450c1fc978ba2757c1ef43.jpeg)


