Kotlin高仿微信-项目实践58篇详细讲解了各个功能点,包括:注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。
效果图:
实现代码:
<?xml version="1.0" encoding="utf-8"?> <layout> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/moment_root" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/wc_base_bg"> <com.aspsine.swipetoloadlayout.SwipeToLoadLayout android:id="@+id/swipeToLoadLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <com.wn.wechatclientdemo.view.CircleRefreshHeaderView android:id="@+id/swipe_refresh_header" android:layout_width="match_parent" android:layout_height="wrap_content" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/swipe_target" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="vertical" /> <com.wn.wechatclientdemo.view.LoaderMoreView android:id="@+id/swipe_load_more_footer" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="20dp" /> </com.aspsine.swipetoloadlayout.SwipeToLoadLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/5/23 16:00 * Description : 朋友圈 */ class DiscoverMomentsFragment : BaseDataBindingFragment<WcDiscoverMomentsBinding>(), OnRefreshListener, OnLoadMoreListener { override fun getLayoutRes() = R.layout.wc_discover_moments private val momentsViewModel : MomentsViewModel by viewModels() private val userViewModel : UserViewModel by viewModels() private var navController: NavController? = null private val REQ_PICTURE_CODE = 101 private val REQ_VIDEO_CODE = 102 private var momentsAdapter : DiscoverMomentsAdapter? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if(!EventBus.getDefault().isRegistered(this)){ EventBus.getDefault().register(this) } swipeToLoadLayout.setOnRefreshListener(this) swipeToLoadLayout.setOnLoadMoreListener(this) var account = DataStoreUtils.getAccount() userViewModel.getUserLocal(account) navController = findNavController() navController?.currentBackStackEntry?.savedStateHandle?.getLiveData<Boolean>(CommonUtils.Moments.PUBLISH_SUCCESS)?.observe(viewLifecycleOwner){ TagUtils.d("朋友圈发布返回") if(it){ momentsViewModel.getMomentsListLocalEvent() } } TagUtils.d("朋友圈重新主页 ") var momentsList = mutableListOf<MomentsBean>() //第一行默认写入个人信息 momentsList.add(MomentsBean()) momentsAdapter = DiscoverMomentsAdapter(momentsList, PopupWindowClick()) userViewModel.userBeanLocal.observe(viewLifecycleOwner){ TagUtils.d("4朋友圈返回用户$it") if(it == null) return@observe momentsAdapter?.setUserBean(it) } //使用EventBus刷新数据 momentsViewModel.getMomentsListLocalEvent() var linearLayoutManager = LinearLayoutManager(requireActivity()) linearLayoutManager.orientation = LinearLayoutManager.VERTICAL swipe_target.layoutManager = linearLayoutManager swipe_target.adapter = momentsAdapter //先加载本地,然后刷新服务器的数据 momentsViewModel.getMomentsListServerEvent() } @Subscribe(threadMode = ThreadMode.MAIN) fun onMessageCallback(momentsEventBean: MomentsEventBean) { if(momentsEventBean == null){ return } if(momentsEventBean.list == null || momentsEventBean.list.size < 1){ return } var list: MutableList<MomentsBean> = momentsEventBean.list if(momentsEventBean.type == MomentsEventBean.TYPE_LOCAL_LIST){ var momentsList = mutableListOf<MomentsBean>() //第一行默认写入个人信息 momentsList.add(MomentsBean()) TagUtils.d("DiscoverFragment onMessageCallback 个数${list.size} ") momentsList.addAll(list) momentsAdapter?.refresh(momentsList) } else { CoroutineScope(Dispatchers.IO).launch { //从服务器获取到数据,插入本地数据库 MomentsRepository.insertMomentListLocal(list) //查询最新的刷新页面 momentsViewModel.getMomentsListLocalEvent() } } } override fun onDestroyView() { super.onDestroyView() TagUtils.d("DiscoverFragment onDestroyView") EventBus.getDefault().unregister(this) } inner class PopupWindowClick : WcOnItemClickInterface { override fun onItemClick(obj: Any) { showPopupWindow() } } private fun showPopupWindow(){ var popupView = layoutInflater.inflate(R.layout.wc_moments_pop_view , moment_root, false) var popupWindow = PopupWindow(popupView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true) popupWindow.showAtLocation(moment_root, Gravity.BOTTOM, 0, 0) var window = requireActivity().window //popupWindow在弹窗的时候背景半透明 val params = window.attributes params.alpha = 0.5f window.attributes = params popupWindow.setOnDismissListener { params.alpha = 1.0f window.attributes = params } //拍照小视频 popupView.findViewById<LinearLayout>(R.id.moments_pop_video).setOnClickListener { popupWindow.dismiss() var bundle = bundleOf(CameraFragment.TYPE_ENTER to CameraFragment.TYPE_MOMENT) navController?.navigate(R.id.action_svideo_camera, bundle) } //选择相册 popupView.findViewById<AppCompatTextView>(R.id.moments_pop_album).setOnClickListener { popupWindow.dismiss() openAblum() } //选择小视频 popupView.findViewById<AppCompatTextView>(R.id.moments_pop_svideo).setOnClickListener { popupWindow.dismiss() openVideo() } //取消 popupView.findViewById<AppCompatTextView>(R.id.moments_pop_cancel).setOnClickListener { popupWindow.dismiss() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQ_PICTURE_CODE && data != null) { val images = data.getStringArrayListExtra(ImageSelector.SELECT_RESULT) val isCameraImage = data.getBooleanExtra(ImageSelector.IS_CAMERA_IMAGE, false) TagUtils.d("ImageSelector", "是否是拍照图片:" + isCameraImage) images?.map { TagUtils.d("返回图片:${it}") } if(images != null && images.size > 0) { var bundle = bundleOf(CommonUtils.Moments.TYPE_NAME to CommonUtils.Moments.TYPE_PICTURE, CommonUtils.Moments.TYPE_IMAGE_PATH to images) navController?.navigate(R.id.action_moments_publish, bundle) } else { TagUtils.d("请选择图片") } } else if (requestCode == REQ_VIDEO_CODE && resultCode == RESULT_OK && null != data) { var selectedVideo = data.getData()!! var filePathColumn = arrayOf(MediaStore.Video.Media.DATA) var cursor : Cursor? = requireActivity().contentResolver.query(selectedVideo, filePathColumn, null, null, null); cursor?.moveToFirst(); var columnIndex : Int? = cursor?.getColumnIndex(filePathColumn[0]); var videoPath = cursor?.getString(columnIndex!!); cursor?.close(); TagUtils.d("选择小视频路径:${videoPath}") var bundle = bundleOf(CommonUtils.Moments.TYPE_NAME to CommonUtils.Moments.TYPE_VIDEO, CommonUtils.Moments.TYPE_VIDEO_PATH to videoPath) navController?.navigate(R.id.action_moments_publish, bundle) } } fun openAblum(){ // 打开相册 ImageSelector.builder() .useCamera(false) // 设置是否使用拍照 .setSingle(false) //设置是否单选 .canPreview(true) //是否点击放大图片查看,,默认为true .start(this,REQ_PICTURE_CODE) } //选择小视频 fun openVideo(){ val i = Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) startActivityForResult(i, REQ_VIDEO_CODE) } override fun onRefresh() { CoroutineScope(Dispatchers.Main).launch { delay(200) swipeToLoadLayout.isRefreshing = false } } override fun onLoadMore() { CoroutineScope(Dispatchers.Main).launch { delay(200) swipeToLoadLayout.isLoadingMore = false } } }
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/5/26 17:55 * Description : 朋友圈适配器 */ class DiscoverMomentsAdapter(var list : MutableList<MomentsBean>, var userClick : WcOnItemClickInterface) : RecyclerView.Adapter<DiscoverMomentsAdapter.DiscoverMomentsHolder>(){ private var userBean : UserBean? = null override fun getItemCount() = list.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiscoverMomentsHolder { var view = LayoutInflater.from(parent.context).inflate(R.layout.wc_discover_moments_item, parent, false) return DiscoverMomentsHolder(view) } override fun onBindViewHolder(holder: DiscoverMomentsHolder, position: Int) { //TagUtils.d("朋友圈position = ${position} , ${userBean}" ) if(position == 0){ //朋友圈个人头像信息 holder.personalView.visibility = View.VISIBLE holder.momentsView.visibility = View.GONE userBean?.let { holder.personalView.process(it, userClick) } } else { holder.personalView.visibility = View.GONE holder.momentsView.visibility = View.VISIBLE holder.momentsView.process(list.get(position)) } } fun setUserBean(userBean: UserBean) { this.userBean = userBean notifyDataSetChanged() } //下拉刷新 fun refresh(list : MutableList<MomentsBean>){ this.list = list notifyDataSetChanged() } //加载更多 fun add(list : MutableList<MomentsBean>){ this.list.addAll(list) notifyDataSetChanged() } class DiscoverMomentsHolder(itemView : View) : RecyclerView.ViewHolder(itemView) { var momentsView = itemView.findViewById<DiscoverMomentsView>(R.id.discover_moments_item_view) var personalView = itemView.findViewById<DiscoverMomentsPersonalView>(R.id.discover_moments_item_personal_view) } }
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/5/26 15:29 * Description : 朋友圈自定义view */ class DiscoverMomentsView : RelativeLayout{ 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 { LayoutInflater.from(context).inflate(R.layout.wc_discover_moments_view, this) } fun process(momentsBean: MomentsBean){ momentsBean?.let { discover_moments_content.text = it.content discover_moments_time.text = CommonUtils.Date.getCurrentDate(it.addTime) BaseUtils.showAvatar(momentsBean.account, discover_moments_icon, discover_moments_name) if(it.type == CommonUtils.Moments.TYPE_PICTURE){ //图片 discover_moments_recyclerview.visibility = View.VISIBLE discover_moments_video.visibility = View.GONE discover_moments_video_icon.visibility = View.GONE //TagUtils.d("朋友圈图片:" + it.images) var imagesList = it.images.split(CommonUtils.Moments.FILE_PATH_MARK) var adapter = MomentsPublishAdapter(2, imagesList, object : WcOnItemClickInterface{ override fun onItemClick(obj: Any) { TagUtils.d("点击:${obj}") var imagePath = obj as String var bundle = bundleOf(CommonUtils.Moments.TYPE_IMAGE_PATH to imagePath, CommonUtils.Moments.TYPE_NAME to CommonUtils.Moments.TYPE_PICTURE, CommonUtils.Chat.IS_HIDE_CONFIRM to true) Navigation.findNavController(discover_moments_recyclerview).navigate(R.id.action_svideo_play, bundle) } }) var linearLayoutManager = GridLayoutManager(context , 3) //linearLayoutManager.orientation = LinearLayoutManager.HORIZONTAL discover_moments_recyclerview.layoutManager = linearLayoutManager discover_moments_recyclerview.adapter = adapter } else if(it.type == CommonUtils.Moments.TYPE_VIDEO){ //小视频 discover_moments_recyclerview.visibility = View.GONE if(File(momentsBean.videoLocal).exists()){ TagUtils.d("本地小视频存在:${momentsBean.videoLocal}") //小视频文件存在, 直接显示播放 discover_moments_video_icon.visibility = View.GONE discover_moments_video.visibility = View.VISIBLE playVideo(momentsBean.videoLocal) discover_moments_video.setOnClickListener { showVideo(discover_moments_video, momentsBean.videoLocal) } } else { //小视频文件不存在, 默认先显示缩略图, 然后下载 discover_moments_video_icon.visibility = View.VISIBLE discover_moments_video.visibility = View.GONE GlideUtils.load(discover_moments_video_icon, CommonUtils.Base.getReallyImage(momentsBean.images)) var videoUrl = CommonUtils.Moments.getReallyImageUrl(momentsBean.video) TagUtils.d("小视频地址:${momentsBean.video} , ${videoUrl}") TagUtils.d("小视频缩略图:${momentsBean.images} , ${CommonUtils.Base.getReallyImage(momentsBean.images)}") var videoFile = FileUtils.getBaseFile(momentsBean.video) VideoDownloadManager.download(videoUrl, videoFile, object : VideoDownloadInter{ override fun onDone(filePath: String) { TagUtils.d("小视频下载完成:${filePath}") CoroutineScope(Dispatchers.Main).launch { discover_moments_video_icon.visibility = View.GONE discover_moments_video.visibility = View.VISIBLE momentsBean.videoLocal = filePath playVideo(filePath) CoroutineScope(Dispatchers.IO).launch { //更新到本地数据库 MomentsRepository.updateMomentLocal(momentsBean) } discover_moments_video.setOnClickListener { showVideo(discover_moments_video, momentsBean.videoLocal) } } } override fun onError() { CoroutineScope(Dispatchers.Main).launch { TagUtils.d("小视频下载失败:${momentsBean.video}") } } override fun onProgress(process: Int) { } }) } } else { //文字 discover_moments_recyclerview.visibility = View.GONE discover_moments_video.visibility = View.GONE } } } //点击查看小视频 private fun showVideo(view: View, videoPath : String){ var bundle = bundleOf(CommonUtils.Moments.TYPE_VIDEO_PATH to videoPath, CommonUtils.Moments.TYPE_NAME to CommonUtils.Moments.TYPE_VIDEO, CommonUtils.Chat.IS_HIDE_CONFIRM to true) Navigation.findNavController(view).navigate(R.id.action_svideo_play, bundle) } /** * 播放小视频 * @param filePath String */ private fun playVideo(filePath : String){ discover_moments_video.setVideoPath(filePath) discover_moments_video.requestFocus() discover_moments_video.start() discover_moments_video.setOnCompletionListener { discover_moments_video.start() } } }