概述
内容
人脸匹配好友列表
需求
- 算法检测出的人脸配到当前的好友列表
- 如果有好友的人脸显示好友名称
过程
设置备注页
Activity
新建一个activity
文件用于展示当前点击的头像
layout/activity_user_face.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.userface.UserFaceActivity">
<androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_user_name" android:name="com.danale.edge.ui.userface.UserFaceFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:layout="@layout/fragment_user_face" />
</androidx.constraintlayout.widget.ConstraintLayout>
|
com/danale/edge/ui/userface/UserFaceActivity.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package com.danale.edge.ui.userface
import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import com.danale.edge.R import com.danale.edge.base.BaseActivity import com.danale.edge.base.BaseNavigation.Constants import com.danale.edge.ui.userface.UserFaceViewModel.Companion.REQUEST_CODE import com.danale.edge.usersdk.usecase.UserSdkUseCase import com.google.gson.Gson import dagger.hilt.android.AndroidEntryPoint import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.schedulers.Schedulers import java.nio.charset.StandardCharsets import javax.inject.Inject
@AndroidEntryPoint class UserFaceActivity : BaseActivity() {
private val viewModel: UserFaceViewModel by viewModels()
@Inject lateinit var userSdk: UserSdkUseCase
@Inject lateinit var gson: Gson
private var mainPosition: Int? = 0
companion object { const val CURRENT_USER_FACE = "CURRENT_USER_FACE" }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_face) }
@SuppressLint("CheckResult") override fun onResume() { super.onResume()
mainPosition = intent?.getIntExtra(Constants.EXTRA_SEARCH_MAIN_POSITION, 0) viewModel.loadSearchHistoryItems(mainPosition)
userSdk.loadAssistData(CURRENT_USER_FACE) .subscribeOn(Schedulers.io()) .subscribeBy(onSuccess = { val cb = StandardCharsets.UTF_8.decode(it) val json = cb.toString() logger.d( TAG, "decode, buffer=${it?.remaining()}, json=${json}" ) val mapData = gson.fromJson(json, MutableMap::class.java) val name = mapData[mainPosition.toString()] viewModel.setName(name as String?) }, onError = { logger.w(TAG, "load assist, buffer, error", it) }) }
@Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_CODE -> { if (resultCode == Activity.RESULT_OK) { if (data != null) { viewModel.setName(data.getStringExtra(Constants.EXTRA_SEARCH_MAIN_POSITION)) } } } } } }
|
Fragment
layout/fragment_user_face.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| <?xml version="1.0" encoding="utf-8"?> <layout 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">
<data>
<variable name="viewModel" type="com.danale.edge.ui.userface.UserFaceViewModel" />
<import type="android.view.View" /> </data>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@mipmap/art_bg_flow" tools:context=".ui.userface.UserFaceActivity">
<com.danale.edge.ui.common.view.TopSafeAreaGuideLine android:id="@+id/safe_area_top_guide" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_begin="24dp" tools:ignore="MissingConstraints" />
<include android:id="@+id/component_title_bar" layout="@layout/component_title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/safe_area_top_guide" />
<androidx.appcompat.widget.LinearLayoutCompat android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" tools:ignore="MissingConstraints">
<androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_persons" android:layout_width="match_parent" android:layout_height="wrap_content" android:adapter="@{viewModel.searchPersonAdapter}" tools:background="#AFBC9D" />
<TextView android:id="@+id/tv_user_nickname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="@{viewModel.currentUserName}" android:textColor="@color/app_std_text_white" android:textSize="18sp" tools:text="Alcidae Username" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_rounded_button" android:backgroundTint="@color/app_primary" android:onClick="@{viewModel::onClickSetFriend}" android:paddingHorizontal="16dp" android:paddingVertical="8dp" android:text="@string/text_replay" android:textColor="@color/app_std_text_primary_inverse" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout> </layout>
|
com/danale/edge/ui/userface/UserFaceFragment.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| package com.danale.edge.ui.userface
import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get import androidx.recyclerview.widget.LinearLayoutManager import com.danale.edge.R import com.danale.edge.base.BaseFragment import com.danale.edge.databinding.FragmentUserFaceBinding
class UserFaceFragment : BaseFragment() { private lateinit var viewModel: UserFaceViewModel lateinit var binding: FragmentUserFaceBinding
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View {
viewModel = ViewModelProvider(requireActivity()).get() binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user_face, container, false) binding.viewModel = this.viewModel binding.lifecycleOwner = viewLifecycleOwner
binding.recyclerPersons.apply { layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) }
viewModel = ViewModelProvider(requireActivity()).get() binding.viewModel = this.viewModel
return binding.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { observeNavigation(viewModel) }
override fun onResume() { super.onResume() setStatusBarDarkText(false, inverseOnNightMode = false) } }
|
![image]()
ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| package com.danale.edge.ui.userface
import android.annotation.SuppressLint import android.app.Application import android.content.Intent import android.view.View import androidx.databinding.ObservableField import androidx.lifecycle.viewModelScope import com.danale.edge.base.BaseNavigation import com.danale.edge.base.BaseViewModel import com.danale.edge.intelligence.IntelligenceRepository import com.danale.edge.ui.common.adapter.PersonAdapter import com.danale.edge.ui.common.adapter.ThumbnailAdapter import com.danale.edge.ui.common.callback.RecyclerItemOnClick import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject
@HiltViewModel class UserFaceViewModel @Inject constructor(application: Application) : BaseViewModel(application), RecyclerItemOnClick<ThumbnailAdapter.ThumbnailViewHolder> {
@Inject lateinit var ai: IntelligenceRepository
var currentUserName = ObservableField("")
private var mainPosition = 0
companion object { const val REQUEST_CODE = 1 }
val searchPersonAdapter = PersonAdapter(itemOnClick = object : RecyclerItemOnClick<PersonAdapter.PersonViewHolder> { override fun onClickItem(viewHolder: PersonAdapter.PersonViewHolder) { logger.d(TAG, "onClickPerson=$viewHolder") } })
@SuppressLint("NotifyDataSetChanged") fun loadSearchHistoryItems(position: Int?) { if (position != null) { mainPosition = position } viewModelScope.launch { val faceUsers = withContext(Dispatchers.IO) { ai.getFaceUserList() } searchPersonAdapter.list.clear() searchPersonAdapter.list.add(faceUsers[position!!]) searchPersonAdapter.notifyDataSetChanged() } }
@SuppressLint("NotifyDataSetChanged") fun setName(name: String?) { logger.d(TAG, "setName, name = $name") currentUserName.set(name) }
@Suppress("UNUSED_PARAMETER") fun onClickSetFriend(view: View) { val intent = Intent().apply { putExtra( BaseNavigation.Constants.EXTRA_SEARCH_MAIN_POSITION, mainPosition, ) } navigationRequiredEvent.postValue( BaseNavigation( BaseNavigation.Route.USER_FACE_FRIEND_LIST, intent, true, REQUEST_CODE ) ) }
override fun onClickItem(viewHolder: ThumbnailAdapter.ThumbnailViewHolder) { logger.d(TAG, "viewHolder=$viewHolder") } }
|
这里的跳转用到了onActivityResult
, 使用的是BaseNavigation
封装后的方法,第三个参数为是否开启startForResult
,第四个参数是标志
封装的方法如下:
1 2 3 4 5 6 7 8 9 10
| fun launch(fromActivity: Activity, intent: Intent? = null) { val finalIntent = Intent(fromActivity, route.activityClass) paramIntent?.let { finalIntent.putExtras(it) } intent?.let { finalIntent.putExtras(it) } if (startForResult) { fromActivity.startActivityForResult(finalIntent, requestCode, requestBundle) } else { fromActivity.startActivity(finalIntent) } }
|
每次跳转都会走到launch
函数中
好友列表页
Activity
com/danale/edge/ui/userface/UserFaceFriendListActivity.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| package com.danale.edge.ui.userface
import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import com.danale.edge.R import com.danale.edge.base.BaseActivity import com.danale.edge.base.BaseNavigation import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.*
@AndroidEntryPoint class UserFaceFriendListActivity : BaseActivity() {
private val viewModel: UserFaceFriendListViewModel by viewModels()
@SuppressLint("SuspiciousIndentation") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_face_friend_list)
val mainPosition = intent?.getIntExtra(BaseNavigation.Constants.EXTRA_SEARCH_MAIN_POSITION, 0) if (mainPosition != null) { viewModel.setPosition(mainPosition) }
MainScope().launch { viewModel.loadPlatformContactList() } }
override fun onResume() { super.onResume() viewModel.navigationRequiredEvent.observe(this) { it?.launch(this) } observeNavigation(viewModel)
viewModel.liveData.observe(this) { val intent = Intent().apply { putExtra( BaseNavigation.Constants.EXTRA_SEARCH_MAIN_POSITION, it, ) }
setResult(Activity.RESULT_OK, intent) finish() } } }
|
跳转的逻辑写在了ViewModel
中,无法调用Acitvity
中的setResult
和finish
方法,因此通过一个liveData
传值通知Activity
进行相关操作,ViewModel与Activity/Fragments通讯的正确姿势 - 掘金 (juejin.cn)
layout/activity_user_face_friend_list.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.userface.UserFaceFriendListActivity">
<androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_user_name" android:name="com.danale.edge.ui.userface.UserFaceFriendListFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:layout="@layout/fragment_user_face_friend_list" />
</androidx.constraintlayout.widget.ConstraintLayout>
|
Fragment
layout/fragment_user_face_friend_list.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?xml version="1.0" encoding="utf-8"?> <layout 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">
<data>
<variable name="viewModel" type="com.danale.edge.ui.userface.UserFaceFriendListViewModel" /> </data>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/app_std_background_gray">
<com.danale.edge.ui.common.view.TopSafeAreaGuideLine android:id="@+id/safe_area_top_guide" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_begin="24dp" tools:ignore="MissingConstraints" />
<include android:id="@+id/component_title_bar" layout="@layout/component_title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/safe_area_top_guide" />
<androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="0dp" android:adapter="@{viewModel.starUserAdapter}" android:clipToPadding="false" android:overScrollMode="never" android:paddingBottom="24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/component_title_bar" />
</androidx.constraintlayout.widget.ConstraintLayout> </layout>
|
com/danale/edge/ui/userface/UserFaceFriendListFragment.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.danale.edge.ui.userface
import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get import androidx.recyclerview.widget.LinearLayoutManager import com.danale.edge.R import com.danale.edge.base.BaseFragment import com.danale.edge.databinding.FragmentUserFaceFriendListBinding
class UserFaceFriendListFragment : BaseFragment() {
private lateinit var binding: FragmentUserFaceFriendListBinding private lateinit var viewModel: UserFaceFriendListViewModel
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate( inflater, R.layout.fragment_user_face_friend_list, container, false ) binding.lifecycleOwner = viewLifecycleOwner
binding.componentTitleBar.apply { leftIconRes = R.mipmap.ic_back_black responder = this@UserFaceFriendListFragment }
binding.recyclerView.apply { layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) itemAnimator?.changeDuration = 0L }
viewModel = ViewModelProvider(requireActivity()).get() binding.viewModel = this.viewModel
return binding.root }
override fun onResume() { super.onResume() setStatusBarDarkText(true) }
}
|
![image]()
ViewModel
com/danale/edge/ui/userface/UserFaceFriendListViewModel.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
| package com.danale.edge.ui.userface
import android.annotation.SuppressLint import android.app.Application import androidx.lifecycle.MutableLiveData import com.danale.edge.base.BaseViewModel import com.danale.edge.ui.common.adapter.ThumbnailAdapter import com.danale.edge.ui.common.callback.RecyclerItemOnClick import com.danale.edge.ui.common.groupie.UserFaceFriendItem import com.danale.edge.usersdk.usecase.UserSdkUseCase import com.google.gson.Gson import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.Section import dagger.hilt.android.lifecycle.HiltViewModel import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.rx3.awaitLast import kotlinx.coroutines.rx3.awaitSingleOrNull import kotlinx.coroutines.withContext import org.json.JSONException import org.json.JSONObject import java.nio.charset.StandardCharsets import javax.inject.Inject
@HiltViewModel class UserFaceFriendListViewModel @Inject constructor(application: Application) : BaseViewModel(application), RecyclerItemOnClick<ThumbnailAdapter.ThumbnailViewHolder> {
val starUserAdapter = GroupieAdapter()
data class UserFaceFriend( var name: String?, var url: String?, var nameId: String? )
private var userFaceFriendList = ArrayList<UserFaceFriend>()
private var mainPosition = 0
@Inject lateinit var userSdk: UserSdkUseCase
@Inject lateinit var gson: Gson
val liveData = MutableLiveData<String>()
companion object { const val CURRENT_USER_FACE = "CURRENT_USER_FACE" }
fun setPosition(position: Int) { mainPosition = position }
suspend fun loadPlatformContactList(refresh: Boolean = false) { try { withContext(Dispatchers.IO) { sdk.getFriendsList(forceReFetch = refresh).awaitLast().map { val displayName = if (it.remarkName?.isNotEmpty() == true) { it.remarkName } else { it.likeName }
val url = withContext(Dispatchers.IO) { it.userName?.let { it1 -> sdk.getPhotoUrlByPlatformUserName(it1).awaitSingleOrNull() } }
userFaceFriendList.add( UserFaceFriend( displayName, url, it.userName ) ) } }
buildSettingItems()
} catch (e: Exception) { toastError(e) }
}
@SuppressLint("CheckResult") private fun buildSettingItems() { logger.d(TAG, "buildSettingItems starFriendList=${userFaceFriendList}") starUserAdapter.apply { add(Section().apply { for ((i, item) in userFaceFriendList.withIndex()) { item.url?.let { url -> item.name?.let { name -> UserFaceFriendItem( avatar = url, name = name, clickListener = { userSdk.loadAssistData(CURRENT_USER_FACE) .subscribeOn(Schedulers.io()) .subscribeBy(onSuccess = { val cb = StandardCharsets.UTF_8.decode(it) val json = cb.toString()
val mapJson = if (json == "null") { val data = mapOf("$mainPosition" to name) gson.toJson(data) } else { val mapData = getMap(json) mapData?.set(mainPosition.toString(), name) gson.toJson(mapData) }
userSdk.storeAssistData( CURRENT_USER_FACE, mapJson.toByteArray(StandardCharsets.UTF_8) ) .subscribeOn(Schedulers.io()) .subscribeBy(onSuccess = { logger.d(TAG, "updateFaceUserList, buffer, success") }, onError = { e -> logger.e(TAG, "updateFaceUserList, buffer, error",e) }) liveData.postValue(mainPosition.toString())
}, onError = { logger.w(TAG, "load assist, buffer, error", it) }) } ) } }?.let { add(it) } } }) } }
private fun getMap(jsonString: String?): HashMap<String, Any>? { val jsonObject: JSONObject try { jsonObject = jsonString?.let { JSONObject(it) }!! val keyIter: Iterator<String> = jsonObject.keys() var key: String var value: Any val valueMap = HashMap<String, Any>() while (keyIter.hasNext()) { key = keyIter.next() value = jsonObject[key] as Any valueMap[key] = value } return valueMap } catch (e: JSONException) { e.printStackTrace() } return null }
override fun onClickItem(viewHolder: ThumbnailAdapter.ThumbnailViewHolder) { logger.d(TAG, "viewHolder=$viewHolder") } }
|
结果