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


效果图:

android 仿微信二维码扫一扫动画 模拟微信扫一扫_android

实现代码:


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <SurfaceView
        android:id="@+id/preview_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_gravity="center"/>

    <com.wn.wechatclientdemo.qrcodescan.ViewfinderView
        android:id="@+id/viewfinder_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</RelativeLayout>
</layout>
 
 
/**
 * Author : wangning
 * Email : maoning20080809@163.com
 * Date : 2022/5/19 18:01
 * Description :
 */
class ViewfinderView : View {

    /**
     * 刷新界面的时间
     */
    private val ANIMATION_DELAY = 10L
    private val OPAQUE = 0xFF

    /**
     * 四个绿色边角对应的长度
     */
    private var ScreenRate = 0

    /**
     * 四个绿色边角对应的宽度
     */
    private val CORNER_WIDTH = 5

    /**
     * 扫描框中的中间线的宽度
     */
    private val MIDDLE_LINE_WIDTH = 6

    /**
     * 扫描框中的中间线的与扫描框左右的间隙
     */
    private val MIDDLE_LINE_PADDING = 5

    /**
     * 中间那条线每次刷新移动的距离
     */
    private val SPEEN_DISTANCE = 5

    /**
     * 手机的屏幕密度
     */
    private var density = 0f

    /**
     * 字体大小
     */
    private val TEXT_SIZE = 16

    /**
     * 字体距离扫描框下面的距离
     */
    private val TEXT_PADDING_TOP = 30

    /**
     * 画笔对象的引用
     */
    private var paint: Paint? = null

    /**
     * 中间滑动线的最顶端位置
     */
    private var slideTop = 0

    /**
     * 中间滑动线的最底端位置
     */
    private var slideBottom = 0

    /**
     * 将扫描的二维码拍下来,这里没有这个功能,暂时不考虑
     */
    private var resultBitmap: Bitmap? = null
    private var maskColor = 0
    private var resultColor = 0

    private var resultPointColor = 0
    private var possibleResultPoints: MutableCollection<ResultPoint>? = null
    private var lastPossibleResultPoints: Collection<ResultPoint>? = null

    var isFirst = false

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs){
        density = context.resources.displayMetrics.density
        //将像素转换成dp
        ScreenRate = (15 * density).toInt()
        paint = Paint()
        val resources = resources
        maskColor = resources.getColor(R.color.viewfinder_mask)
        resultColor = resources.getColor(R.color.result_view)
        resultPointColor = resources.getColor(R.color.possible_result_points)
        possibleResultPoints = HashSet(5)
    }

    override fun onDraw(canvas: Canvas) {
        //中间的扫描框,你要修改扫描框的大小,去CameraManager里面修改
        val frame: Rect = CameraManager.get()?.getFramingRect() ?: return

        //初始化中间线滑动的最上边和最下边
        if (!isFirst) {
            isFirst = true
            slideTop = frame.top
            slideBottom = frame.bottom
        }

        //获取屏幕的宽和高
        val width = canvas.width
        val height = canvas.height
        paint!!.color = if (resultBitmap != null) resultColor else maskColor

        //画出扫描框外面的阴影部分,共四个部分,扫描框的上面到屏幕上面,扫描框的下面到屏幕下面
        //扫描框的左边面到屏幕左边,扫描框的右边到屏幕右边
        canvas.drawRect(0f, 0f, width.toFloat(), frame.top.toFloat(), paint!!)
        canvas.drawRect(
            0f, frame.top.toFloat(), frame.left.toFloat(), (frame.bottom + 1).toFloat(),
            paint!!
        )
        canvas.drawRect(
            (frame.right + 1).toFloat(),
            frame.top.toFloat(),
            width.toFloat(),
            (frame.bottom + 1).toFloat(),
            paint!!
        )
        canvas.drawRect(
            0f,
            (frame.bottom + 1).toFloat(),
            width.toFloat(),
            height.toFloat(),
            paint!!
        )
        if (resultBitmap != null) {
            // Draw the opaque result bitmap over the scanning rectangle
            paint!!.alpha = OPAQUE
            canvas.drawBitmap(resultBitmap!!, frame.left.toFloat(), frame.top.toFloat(), paint)
        } else {

            //画扫描框边上的角,总共8个部分
            paint!!.color = Color.GREEN
            canvas.drawRect(
                frame.left.toFloat(), frame.top.toFloat(), (frame.left + ScreenRate).toFloat(), (
                        frame.top + CORNER_WIDTH).toFloat(),
                paint!!
            )
            canvas.drawRect(
                frame.left.toFloat(),
                frame.top.toFloat(),
                (frame.left + CORNER_WIDTH).toFloat(),
                (frame.top
                        + ScreenRate).toFloat(),
                paint!!
            )
            canvas.drawRect(
                (frame.right - ScreenRate).toFloat(), frame.top.toFloat(), frame.right.toFloat(), (
                        frame.top + CORNER_WIDTH).toFloat(),
                paint!!
            )
            canvas.drawRect(
                (frame.right - CORNER_WIDTH).toFloat(),
                frame.top.toFloat(),
                frame.right.toFloat(),
                (frame.top
                        + ScreenRate).toFloat(),
                paint!!
            )
            canvas.drawRect(
                frame.left.toFloat(), (frame.bottom - CORNER_WIDTH).toFloat(), (frame.left
                        + ScreenRate).toFloat(), frame.bottom.toFloat(), paint!!
            )
            canvas.drawRect(
                frame.left.toFloat(), (frame.bottom - ScreenRate).toFloat(), (
                        frame.left + CORNER_WIDTH).toFloat(), frame.bottom.toFloat(), paint!!
            )
            canvas.drawRect(
                (frame.right - ScreenRate).toFloat(), (frame.bottom - CORNER_WIDTH).toFloat(),
                frame.right.toFloat(), frame.bottom.toFloat(), paint!!
            )
            canvas.drawRect(
                (frame.right - CORNER_WIDTH).toFloat(), (frame.bottom - ScreenRate).toFloat(),
                frame.right.toFloat(), frame.bottom.toFloat(), paint!!
            )


            //绘制中间的线,每次刷新界面,中间的线往下移动SPEEN_DISTANCE
            slideTop += SPEEN_DISTANCE
            if (slideTop >= frame.bottom) {
                slideTop = frame.top
            }
            val lineRect = Rect()
            lineRect.left = frame.left
            lineRect.right = frame.right
            lineRect.top = slideTop
            lineRect.bottom = slideTop + 18
            canvas.drawBitmap(
                (resources.getDrawable(R.drawable.qrcode_scan_line) as BitmapDrawable).bitmap,
                null,
                lineRect,
                paint
            )

            //画扫描框下面的字
            paint!!.color = Color.WHITE
            paint!!.textSize = TEXT_SIZE * density
            paint!!.alpha = 0x40
            paint!!.typeface = Typeface.create("System", Typeface.BOLD)
            val text = resources.getString(R.string.scan_text)
            val textWidth = paint!!.measureText(text)
            canvas.drawText(
                text, (width - textWidth) / 2, frame.bottom + TEXT_PADDING_TOP.toFloat() * density,
                paint!!
            )
            val currentPossible: Collection<ResultPoint>? = possibleResultPoints
            val currentLast = lastPossibleResultPoints
            if (currentPossible!!.isEmpty()) {
                lastPossibleResultPoints = null
            } else {
                possibleResultPoints = HashSet(5)
                lastPossibleResultPoints = currentPossible
                paint!!.alpha = OPAQUE
                paint!!.color = resultPointColor
                for (point in currentPossible) {
                    canvas.drawCircle(
                        frame.left + point.x, frame.top
                                + point.y, 6.0f, paint!!
                    )
                }
            }
            if (currentLast != null) {
                paint!!.alpha = OPAQUE / 2
                paint!!.color = resultPointColor
                for (point in currentLast) {
                    canvas.drawCircle(
                        frame.left + point.x, frame.top
                                + point.y, 3.0f, paint!!
                    )
                }
            }

            //只刷新扫描框的内容,其他地方不刷新
            postInvalidateDelayed(
                ANIMATION_DELAY, frame.left, frame.top,
                frame.right, frame.bottom
            )
        }
    }

    fun drawViewfinder() {
        resultBitmap = null
        invalidate()
    }

    /**
     * Draw a bitmap with the result points highlighted instead of the live
     * scanning display.
     *
     * @param barcode
     * An image of the decoded barcode.
     */
    fun drawResultBitmap(barcode: Bitmap?) {
        resultBitmap = barcode
        invalidate()
    }

    fun addPossibleResultPoint(point: ResultPoint) {
        possibleResultPoints!!.add(point)
    }

}
 
 
/**
 * Author : wangning
 * Email : maoning20080809@163.com
 * Date : 2022/5/19 18:10
 * Description : 扫一扫
 */
class QRCodeScanFragment : BaseDataBindingFragment<QrcodeActivityCaptureBinding>(), Callback{

    private var handler: CaptureActivityHandler? = null
    private var viewfinderView: ViewfinderView? = null
    private var hasPermission = false
    private var hasSurface = false
    private var decodeFormats: Vector<BarcodeFormat>? = null
    private var characterSet: String? = null
    private var inactivityTimer: InactivityTimer? = null
    private var mediaPlayer: MediaPlayer? = null
    private var playBeep = false
    private val BEEP_VOLUME = 0.10f
    private var vibrate = false

    private val REQUEST_CODE = 100
    private val REQUEST_CAMERA_CODE = 101
    private val PARSE_BARCODE_SUC = 300
    private val PARSE_BARCODE_FAIL = 303
    private var mProgress: ProgressDialog? = null
    private var photo_path: String? = null
    private var scanBitmap: Bitmap? = null
    private var navCollection : NavController? = null

    override fun getLayoutRes() = R.layout.qrcode_activity_capture

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        TagUtils.d("扫描 onViewCreated ")
        navCollection = findNavController()
        handlePermission()
    }

    fun init() {
        TagUtils.d("扫描 init ")
        CameraManager.init(requireActivity())
        viewfinderView = viewfinder_view
        hasSurface = false
        inactivityTimer = InactivityTimer(requireActivity())
    }

    companion object {
        fun onOpen(activity: Activity){
            var intent = Intent(activity, QRCodeScanFragment::class.java)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            WcApp.getContext().startActivity(intent)
        }
    }

    private fun handlePermission() {
        if(ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
            requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_CODE)
        } else {
            hasPermission = true
            init()
        }
    }

    override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if(requestCode == REQUEST_CAMERA_CODE && grantResults != null && grantResults.size > 0){
            if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
                TagUtils.d("扫描 onRequestPermissionsResult ")
                hasPermission = true
                init()
                val surfaceHolder = preview_view.holder
                initCamera(surfaceHolder)
            }
        }
    }

//    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
//        /**
//         * 此方法用于初始化菜单,其中menu参数就是即将要显示的Menu实例。 返回true则显示该menu,false 则不显示;
//         * (只会在第一次初始化菜单时调用) Inflate the menu; this adds items to the action bar
//         * if it is present.
//         */
//        menuInflater.inflate(R.menu.qrcode_activity_main, menu)
//        return true
//    }

//    override fun onOptionsItemSelected(item: MenuItem): Boolean {
//        /**
//         * 菜单项被点击时调用,也就是菜单项的监听方法。
//         * 通过这几个方法,可以得知,对于Activity,同一时间只能显示和监听一个Menu 对象。 TODO Auto-generated
//         * method stub
//         */
//        when (item.itemId) {
//            R.id.menu_settings -> {
//                //打开手机中的相册
//                val innerIntent =
//                    Intent(Intent.ACTION_GET_CONTENT) //"android.intent.action.GET_CONTENT"
//                innerIntent.type = "image/*"
//                val wrapperIntent = Intent.createChooser(innerIntent, "选择二维码图片")
//                this.startActivityForResult(wrapperIntent, REQUEST_CODE)
//            }
//        }
//        return super.onOptionsItemSelected(item)
//    }

    private val mHandler: Handler = MyHandler(this)

    inner class MyHandler(activity: QRCodeScanFragment) : Handler() {
        private val activityReference: WeakReference<QRCodeScanFragment>
        override fun handleMessage(msg: Message) {
            val activity = activityReference.get()
            activity!!.mProgress!!.dismiss()
            when (msg.what) {
                PARSE_BARCODE_SUC -> activity.onResultHandler(
                    msg.obj as String,
                    activity.scanBitmap
                )
                PARSE_BARCODE_FAIL -> Toast.makeText(requireActivity(),msg.obj as String,Toast.LENGTH_LONG).show()
            }
            super.handleMessage(msg)
        }

        init {
            activityReference = WeakReference(activity)
        }
    }

    /*override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE -> {
                    //获取选中图片的路径
                    val cursor = contentResolver.query(data.data!!, null, null, null, null)
                    if (cursor!!.moveToFirst()) {
                        photo_path =
                            cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
                    }
                    cursor.close()
                    mProgress = ProgressDialog(this@MipcaActivityCapture)
                    mProgress!!.setMessage("正在扫描...")
                    mProgress!!.setCancelable(false)
                    mProgress!!.show()
                    Thread {
                        val result = scanningImage(photo_path)
                        if (result != null) {
                            val m = mHandler.obtainMessage()
                            m.what = PARSE_BARCODE_SUC
                            m.obj = result.text
                            mHandler.sendMessage(m)
                        } else {
                            val m = mHandler.obtainMessage()
                            m.what = PARSE_BARCODE_FAIL
                            m.obj = "Scan failed!"
                            mHandler.sendMessage(m)
                        }
                    }.start()
                }
            }
        }
    }*/

    /**
     * 扫描二维码图片的方法
     * @param path
     * @return
     */
    fun scanningImage(path: String?): Result? {
        if (TextUtils.isEmpty(path)) {
            return null
        }
        val hints = Hashtable<DecodeHintType, String?>()
        hints[DecodeHintType.CHARACTER_SET] = "UTF8" //设置二维码内容的编码
        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true // 先获取原大小
        scanBitmap = BitmapFactory.decodeFile(path, options)
        options.inJustDecodeBounds = false // 获取新的大小
        var sampleSize = (options.outHeight / 200f).toInt()
        if (sampleSize <= 0) sampleSize = 1
        options.inSampleSize = sampleSize
        scanBitmap = BitmapFactory.decodeFile(path, options)
        val source = RGBLuminanceSource(scanBitmap!!)
        val bitmap1 = BinaryBitmap(HybridBinarizer(source))
        val reader = QRCodeReader()
        try {
            return reader.decode(bitmap1, hints)
        } catch (e: NotFoundException) {
            e.printStackTrace()
        } catch (e: ChecksumException) {
            e.printStackTrace()
        } catch (e: FormatException) {
            e.printStackTrace()
        }
        return null
    }


    override fun onResume() {
        super.onResume()
        TagUtils.d("扫描 onResume ${hasSurface} , ${hasPermission}")
        //没有权限直接返回
        if(!hasPermission){
            return
        }
        //val surfaceView = findViewById<View>(R.id.preview_view) as SurfaceView
        val surfaceView = preview_view
        val surfaceHolder = surfaceView.holder
        if (hasSurface) {
            initCamera(surfaceHolder)
        } else {
            surfaceHolder.addCallback(this)
            surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
        }
        decodeFormats = null
        characterSet = null
        playBeep = true
        val audioService = requireActivity().getSystemService(AUDIO_SERVICE) as AudioManager
        if (audioService.ringerMode != AudioManager.RINGER_MODE_NORMAL) {
            playBeep = false
        }
        initBeepSound()
        vibrate = true
    }

    override fun onPause() {
        super.onPause()
        TagUtils.d("扫描 onPause ${hasPermission}")
        if(!hasPermission){
            return
        }
        handler?.quitSynchronously()
        handler = null
        CameraManager.get()?.closeDriver()
    }

    override fun onDestroy() {
        TagUtils.d("扫描 onDestroy ")
        inactivityTimer?.shutdown()
        super.onDestroy()
    }

    /**
     * 处理扫描结果
     * @param result
     * @param barcode
     */
    fun handleDecode(result: Result, barcode: Bitmap?) {
        inactivityTimer!!.onActivity()
        playBeepSoundAndVibrate()
        val resultString = result.text
        onResultHandler(resultString, barcode)
    }

    /**
     * 跳转到上一个页面
     * @param resultString
     * @param bitmap
     */
    private fun onResultHandler(resultString: String, bitmap: Bitmap?) {
        if (TextUtils.isEmpty(resultString)) {
            Toast.makeText(requireActivity(), "Scan failed!", Toast.LENGTH_SHORT).show()
            return
        }
        TagUtils.d("二维码返回字符串:${resultString}")
        Navigation.findNavController(preview_view).popBackStack()
        if(resultString.startsWith(CommonUtils.QRCommon.QR_RECEIVE_CODE)){
            //向个人付款
            TagUtils.d("跳转向个人付款 ")
            var tempResult = resultString.substring(CommonUtils.QRCommon.QR_RECEIVE_CODE.length, resultString.length)
            var toUser = tempResult.split(":")[1]
            var balance = tempResult.split(":")[0]
            var bundle = bundleOf(CommonUtils.QRCommon.BALANCE to balance.toFloat(), CommonUtils.QRCommon.TO_USER to toUser)
            navCollection?.navigate(R.id.action_personal_payment, bundle)
        } else if(resultString.startsWith(CommonUtils.QRCommon.QR_PAYMENT_CODE)){
            //向商家付款
            var toUser = resultString.substring(CommonUtils.QRCommon.QR_PAYMENT_CODE.length, resultString.length)
            TagUtils.d("跳转向商家付款 ${toUser}")
            var bundle = bundleOf(CommonUtils.QRCommon.TO_USER to toUser)
            navCollection?.navigate(R.id.action_merchant_payment, bundle)
        } else if(resultString.startsWith("http") || resultString.startsWith("https")){
            //跳转到网站
            CommonUtils.Base.goWebsite(resultString)
        } else if(resultString.startsWith(CommonUtils.QRCommon.QR_ADD_FRIEND)){
            var userInfo = resultString.substring(CommonUtils.QRCommon.QR_ADD_FRIEND.length, resultString.length)
            //account+"###"+it.nickName+"###"+it.avatar
            var userInfos = userInfo.split("###")
            var toUser = userInfos[0]
            var nickName = userInfos[1]
            var avatar = userInfos[2]

            var contactsBean = ContactsRepository.getContactsLocalAsync(toUser)
            if(contactsBean != null){
                //如果是好友,直接打开聊天
                var bundle = bundleOf(ChatFragment.USER_ID to toUser)
                navCollection?.navigate(R.id.nav_chat, bundle)
            } else {
                var userBean = UserBean(toUser)
                userBean.avatar = avatar
                userBean.nickName = nickName
                //跳转到添加好友页面
                val bundle = bundleOf("userbean" to userBean)
                navCollection?.navigate(R.id.action_contacts_search_friends_details, bundle)
            }
        } else {
            TagUtils.d("跳转空")
        }
    }

    private fun initCamera(surfaceHolder: SurfaceHolder) {
        TagUtils.d("扫描 initCamera ")

        try {
            CameraManager.get()?.openDriver(surfaceHolder)
        } catch (ioe: IOException) {
            return
        } catch (e: RuntimeException) {
            return
        }
        if (handler == null) {
            handler = CaptureActivityHandler( this, decodeFormats, characterSet )
        }
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int,height: Int) {
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        TagUtils.d("扫描 surfaceCreated ")
        if (!hasSurface) {
            hasSurface = true
            initCamera(holder)
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        hasSurface = false
    }

    fun getViewfinderView(): ViewfinderView? {
        return viewfinderView
    }

    fun getHandler(): Handler? {
        return handler
    }

    fun drawViewfinder() {
        viewfinderView!!.drawViewfinder()
    }

    private fun initBeepSound() {
        if (playBeep && mediaPlayer == null) {
            // The volume on STREAM_SYSTEM is not adjustable, and users found it
            // too loud,
            // so we now play on the music stream.
            requireActivity().volumeControlStream = AudioManager.STREAM_MUSIC
            mediaPlayer = MediaPlayer()
            mediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
            mediaPlayer!!.setOnCompletionListener(beepListener)
            val file = resources.openRawResourceFd(R.raw.beep)
            try {
                mediaPlayer!!.setDataSource(
                    file.fileDescriptor,
                    file.startOffset, file.length
                )
                file.close()
                mediaPlayer!!.setVolume(BEEP_VOLUME, BEEP_VOLUME)
                mediaPlayer!!.prepare()
            } catch (e: IOException) {
                mediaPlayer = null
            }
        }
    }

    private val VIBRATE_DURATION = 200L

    private fun playBeepSoundAndVibrate() {
        if (playBeep && mediaPlayer != null) {
            mediaPlayer!!.start()
        }
        if (vibrate) {
            val vibrator = requireActivity().getSystemService(VIBRATOR_SERVICE) as Vibrator
            vibrator.vibrate(VIBRATE_DURATION)
        }
    }

    /**
     * When the beep has finished playing, rewind to queue up another one.
     */
    private val beepListener =
        OnCompletionListener { mediaPlayer -> mediaPlayer.seekTo(0) }

}