星标用户
MoMo Lv5

概述

内容

实现星标用户的页面

需求

  1. 加载星标用户列表:starUserLoad
  2. 加载好友列表: com.danale.edge.usersdk.usecase.UserSdkUseCase#getFriendsList
    1. 使用好友的用户名获取昵称:com.danale.edge.usersdk.usecase.UserSdkUseCaseImpl#getUserLikeNameByPlatformUserName
    2. 使用好友的用户名获取头像:com.danale.edge.usersdk.usecase.UserSdkUseCaseImpl#getPhotoUrlByPlatformUserName
  3. 每个好友后面有一个ratio,false->true触发starUserStore, true->false -> starUserDelete

过程

定义接口Op

存用户

com/danale/edge/usersdk/operation/StarUserStoreOp.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
package com.danale.edge.usersdk.operation

import com.danale.edge.foundation.log.Logger
import com.danale.edge.usersdk.jni.BaseJsonOperation
import com.danale.edge.usersdk.jni.BaseResponse
import com.danale.edge.usersdk.jni.UserSdkWrapper
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName

class StarUserStoreOp(val param: Param, logger: Logger, gson: Gson) :
BaseJsonOperation<BaseResponse>(logger, gson) {


override fun unmarshallSerializedResponse(jsonOutput: String?): BaseResponse {
return BaseResponse()
}

override fun performOperation(context: CallbackContext) {
UserSdkWrapper.nativeStoreStarUserList(context, gson.toJson(param))
}

class Param(
@SerializedName("star_user_list") val starUserList: List<StarUserListInfo>
)


class StarUserListInfo(
@SerializedName("device_id") val deviceId: String? = null,
@SerializedName("user_name") val userName: String? = null
) {
override fun toString(): String {
return "StarUserStoreOp(deviceId=$deviceId, name=$userName)"
}

}
}

performOperation中定义了接口UserSdkWrapper.nativeStoreStarUserList

1
2
3
4
external fun nativeStoreStarUserList(
context: NativeResponseCallback,
paramJsonString: String
)

在这里调用c++的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C"
JNIEXPORT void JNICALL
Java_com_danale_edge_usersdk_jni_UserSdkWrapper_nativeStoreStarUserList(JNIEnv *env, jobject thiz,
jobject context,
jstring param_json_string) {
jboolean isCopy = JNI_TRUE;
const char *param_json_string_chars = env->GetStringUTFChars(param_json_string,
&isCopy);
jobject ctx_ref = env->NewGlobalRef(context);
APP_LOG_I("nativeStoreStarUserList, ref=%p", ctx_ref);
dip_pub_user_store_star_user(ctx_ref,
const_cast<char *>(param_json_string_chars),
app_dip_fe_usr_sdk_general_json_callback);
env->ReleaseStringUTFChars(param_json_string, param_json_string_chars);
}

dip_pub_user_store_star_user是最终接口的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* @brief 存储星标用户
* @param [in]args_json.star_user_list 星标用户信息数组
* @param [in]args_json.user_list.device_id 设备id
* @param [in]args_json.user_list.user_name 用户名

example:
请求参数示例
{
"star_user_list":[{
"device_id": "xxxxxxxxxxxxxx",
"user_name": "xxxxxxxxxxxxxx",
}]
}
成功返回0,失败返回error code
*/
extern void dip_pub_user_store_star_user(dip_fe_usr_sdk_ctx ctx, char* args_json, dip_fe_usr_sdk_json_callback callback);

取用户

com/danale/edge/usersdk/operation/StarUserLoadOp.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
package com.danale.edge.usersdk.operation

import com.danale.edge.foundation.log.Logger
import com.danale.edge.usersdk.jni.BaseJsonOperation
import com.danale.edge.usersdk.jni.BaseResponse
import com.danale.edge.usersdk.jni.UserSdkWrapper
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName

class StarUserLoadOp(val param: Param, logger: Logger, gson: Gson) :
BaseJsonOperation<StarUserLoadOp.Response>(logger, gson) {


override fun unmarshallSerializedResponse(jsonOutput: String?): Response {
return gson.fromJson(jsonOutput, Response::class.java) ?: Response(emptyList())
}

override fun performOperation(context: CallbackContext) {
UserSdkWrapper.nativeGetStarUserList(context, gson.toJson(param))
}

class Param(
@SerializedName("device_id") var id: String
)

class Response(toList: List<StarUserInfo>) : BaseResponse() {
@SerializedName("star_user_list")
var deviceIdList: List<StarUserInfo>? = null

override fun toString(): String {
return "StarUserLoadOp(deviceIdList=${deviceIdList})"
}
}

class StarUserInfo {
@SerializedName("user_name")
val userName: String = ""

override fun toString(): String {
return "StarUserLoadOp(userName=$userName)"
}
}

}
1
2
3
4
external fun nativeGetStarUserList(
context: NativeResponseCallback,
paramJsonString: String
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C"
JNIEXPORT void JNICALL
Java_com_danale_edge_usersdk_jni_UserSdkWrapper_nativeGetStarUserList(JNIEnv *env, jobject thiz,
jobject context,
jstring param_json_string) {
jboolean isCopy = JNI_TRUE;
const char *param_json_string_chars = env->GetStringUTFChars(param_json_string,
&isCopy);
jobject ctx_ref = env->NewGlobalRef(context);
APP_LOG_I("nativeGetStarUserList, ref=%p", ctx_ref);
dip_pub_user_load_star_user_list(ctx_ref,
const_cast<char *>(param_json_string_chars),
app_dip_fe_usr_sdk_general_json_callback);
env->ReleaseStringUTFChars(param_json_string, param_json_string_chars);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* @brief 存储星标用户
* @param [in]args_json.device_id 设备id
* @param [out]args_json.star_user_list 星标用户列表
* @param [out]args_json.star_user_list.user_name 用户名

example:
请求参数示例
{
"device_id": "xxxxxxxxx",
}
返回参数示例
{
"star_user_list":
[
{"user_name" : "xxxxx"},
{"user_name" : "xxxxx"}
]
}
*/
extern void dip_pub_user_load_star_user_list(dip_fe_usr_sdk_ctx ctx, char *args_json,
dip_fe_usr_sdk_json_callback callback);

删用户

com/danale/edge/usersdk/operation/StarUserDeleteOp.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
package com.danale.edge.usersdk.operation

import com.danale.edge.foundation.log.Logger
import com.danale.edge.usersdk.jni.BaseJsonOperation
import com.danale.edge.usersdk.jni.BaseResponse
import com.danale.edge.usersdk.jni.UserSdkWrapper
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName

class StarUserDeleteOp(val param: Param, logger: Logger, gson: Gson) :
BaseJsonOperation<BaseResponse>(logger, gson) {

override fun unmarshallSerializedResponse(jsonOutput: String?): BaseResponse {
return BaseResponse()
}

override fun performOperation(context: CallbackContext) {
UserSdkWrapper.nativeDeleteStarUserList(context, gson.toJson(param))
}

class Param(
@SerializedName("star_user_list") val starUserList: List<StarUserListInfo>
)


class StarUserListInfo(
@SerializedName("device_id") val deviceId: String? = null,
@SerializedName("user_name") val userName: String? = null
) {
override fun toString(): String {
return "StarUserStoreOp(deviceId=$deviceId, name=$userName)"
}

}
}
1
2
3
4
external fun nativeDeleteStarUserList(
context: NativeResponseCallback,
paramJsonString: String
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C"
JNIEXPORT void JNICALL
Java_com_danale_edge_usersdk_jni_UserSdkWrapper_nativeDeleteStarUserList(JNIEnv *env, jobject thiz,
jobject context,
jstring param_json_string) {
jboolean isCopy = JNI_TRUE;
const char *param_json_string_chars = env->GetStringUTFChars(param_json_string,
&isCopy);
jobject ctx_ref = env->NewGlobalRef(context);
APP_LOG_I("nativeDeleteStarUserList, ref=%p", ctx_ref);
dip_pub_user_delete_star_users(ctx_ref,
const_cast<char *>(param_json_string_chars),
app_dip_fe_usr_sdk_general_json_callback);
env->ReleaseStringUTFChars(param_json_string, param_json_string_chars);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* @brief 批量删除指定星标用户集合
* @param [in]args_json.device_id 设备id
* @param [in]args_json.star_user_list 需要删除的星标用户列表
* @param [in]args_json.star_user_list.user_name 星标用户名

example:
请求参数示例
{
"star_user_list":[{
"device_id": "xxxxxxxxxxxxxx",
"user_name": "xxxxxxxxxxxxxx",
}]
}
删除成功返回0,失败返回相应错误码
*/
extern void dip_pub_user_delete_star_users(dip_fe_usr_sdk_ctx ctx, char *args_json,
dip_fe_usr_sdk_json_callback callback);

定义接口sdk

通过sdk绑定到Op上

com/danale/edge/usersdk/usecase/UserSdkUseCase.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 获取星标好友列表
*/
fun getStarUserList(
id: String,
): Single<StarUserLoadOp.Response>

/**
* 添加星标好友
* */
fun storeStarUser(
deviceId: String,
userName: String
): Completable

/**
* 删除星标好友
* */
fun deleteStarUser(
deviceId: String,
userName: String
): Completable

com/danale/edge/usersdk/usecase/UserSdkUseCaseImpl.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
override fun getStarUserList(
id: String
): Single<StarUserLoadOp.Response> {
return StarUserLoadOp(StarUserLoadOp.Param(id), logger, gsonInst)
}

override fun storeStarUser(deviceId: String, userName: String): Completable {
return StarUserStoreOp(
StarUserStoreOp.Param(
listOf(
StarUserStoreOp.StarUserListInfo(
deviceId,
userName
)
)
), logger, gsonInst
).flatMapCompletable {
Completable.complete()
}
}

override fun deleteStarUser(deviceId: String, userName: String): Completable {
return StarUserDeleteOp(
StarUserDeleteOp.Param(
listOf(
StarUserDeleteOp.StarUserListInfo(
deviceId,
userName
)
)
), logger, gsonInst
).flatMapCompletable {
Completable.complete()
}
}

业务逻辑实现

Fragment

定义用于展示用户列表的Fragment

com/danale/edge/ui/devicecontrol/common/StarUserFragment.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
package com.danale.edge.ui.devicecontrol.common

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.FragmentStarUserBinding

class StarUserFragment : BaseFragment() {

private lateinit var binding: FragmentStarUserBinding
private lateinit var viewModel: StarUserViewModel

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_star_user, container, false)
binding.lifecycleOwner = viewLifecycleOwner

binding.componentTitleBar.apply {
leftIconRes = R.mipmap.ic_back_black
title = getString(R.string.text_star_user)
responder = this@StarUserFragment
}

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

}

定义对应的Fragment绑定

layout/fragment_star_user.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.devicecontrol.common.StarUserViewModel" />
</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>

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
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
193
194
195
196
197
198
199
200
201
202
203
package com.danale.edge.ui.devicecontrol.common

import android.app.Application
import androidx.lifecycle.viewModelScope
import com.danale.edge.base.BaseViewModel
import com.danale.edge.foundation.privacy.Fuzzy
import com.danale.edge.ui.common.callback.RecyclerItemOnClick
import com.danale.edge.ui.common.groupie.StarUserItem
import com.danale.edge.ui.common.recycler.RecyclerListViewHolder
import com.danale.edge.usersdk.operation.StarUserLoadOp
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
import kotlinx.coroutines.rx3.awaitLast
import kotlinx.coroutines.rx3.awaitSingleOrNull
import kotlinx.coroutines.withContext
import javax.inject.Inject

@HiltViewModel
class StarUserViewModel @Inject constructor(application: Application) :
BaseViewModel(application), RecyclerItemOnClick<RecyclerListViewHolder> {

// 用于加载页面的Adapter
val starUserAdapter = GroupieAdapter()

// 获取的星标列表
private var starUserList: StarUserLoadOp.Response? = null

// 用一个map存储获得的星标列表中的用户id
private var starUserMap: MutableMap<String, Boolean> = mutableMapOf()

// 最终列表中的数据格式
data class StarFriend(
var name: String?,
var url: String?,
var isStar: Boolean,
var nameId: String?
)

// 最终求交集的星标列表
private var starFriendList = ArrayList<StarFriend>()

// 设备id
var deviceId32: String? = null
set(value) {
logger.d(TAG, "setDeviceId=${Fuzzy.interval(value)}")
field = value

viewModelScope.launch {
// 获取的好友列表
loadPlatformContactList()
}
}

/**
* 获取星标好友列表
*/
private suspend fun loadStarFriendList() {
starUserList = withContext(Dispatchers.IO) {
try {
deviceId32?.let {
sdk.getStarUserList(it).await()
}
} catch (e: Exception) {
toastError(e)
null
}
}
logger.d(TAG, "loadStarFriendList, starUserList=$starUserList")
}

/**
* 加载好友列表
*/
private suspend fun loadPlatformContactList(refresh: Boolean = false) {
try {
// 获取的星标列表
loadStarFriendList()
val nonNullList = starUserList?.deviceIdList ?: emptyList()
if (nonNullList.isNotEmpty()) nonNullList.forEach {
starUserMap[it.userName] = true
}

// 获取的好友列表
withContext(Dispatchers.IO) {
sdk.getFriendsList(forceReFetch = refresh).awaitLast()?.map { it ->
// 用户名
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()
}
}

// 添加数据
starFriendList.add(
StarFriend(
displayName,
url,
starUserMap[it.userName] !== null,
it.userName
)
)

}
}

logger.i(TAG, "loadPlatformContactList, starFriendList= $starFriendList")

// 构建列表
buildSettingItems()

} catch (e: Exception) {
toastError(e)
}

}

// 构建列表
private fun buildSettingItems() {
starUserAdapter.apply {
add(Section().apply {
for ((i, item) in starFriendList.withIndex()) {
item.url?.let { url ->
item.name?.let { name ->
StarUserItem(
list = starFriendList, // 传入最新的列表获得isStar属性
avatar = url, // 头像链接
name = name, // 用户名
clickListener = { _ ->
if (!item.isStar) {
// 存
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
deviceId32?.let { did ->
item.nameId?.let { nameId ->
sdk.storeStarUser(
did,
nameId
).await()
}
}
// 更新列表
starFriendList[i] =
StarFriend(name, url, true, item.nameId)
} catch (e: Exception) {
logger.d(TAG, "error, $e")
}

}
// 通知Adapter刷新数据
starUserAdapter.notifyItemChanged(i, 0)
}
} else {
// 删
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
deviceId32?.let { did ->
item.nameId?.let { nameId ->
sdk.deleteStarUser(
did,
nameId
).await()
}
}
starFriendList[i] =
StarFriend(name, url, false, item.nameId)
} catch (e: Exception) {
logger.d(TAG, "error, $e")
}

}
starUserAdapter.notifyItemChanged(i, 0)
}
}
}
)
}
}?.let { StarUserItem ->
add(
StarUserItem
)
}
}
})
}
}

override fun onClickItem(viewHolder: RecyclerListViewHolder) {
logger.d(TAG, "onClickItem, viewHolder=$viewHolder")
}
}

StarUserItem

定义一个用于展示用户的组件

com/danale/edge/ui/common/groupie/StarUserItem.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
package com.danale.edge.ui.common.groupie

import android.annotation.SuppressLint
import android.view.View
import androidx.databinding.DataBindingUtil
import coil.load
import coil.transform.RoundedCornersTransformation
import com.danale.edge.R
import com.danale.edge.databinding.RecyclerGroupieItemStarUserBinding
import com.danale.edge.ui.devicecontrol.common.StarUserViewModel
import com.xwray.groupie.viewbinding.BindableItem

class StarUserItem(
var list: ArrayList<StarUserViewModel.StarFriend> = ArrayList(),
var avatar: String,
var name: String = "",
val clickListener: ((View) -> Unit)? = null
) : BindableItem<RecyclerGroupieItemStarUserBinding>() {

@SuppressLint("ClickableViewAccessibility")
override fun bind(viewBinding: RecyclerGroupieItemStarUserBinding, position: Int) {
viewBinding.let {
it.ivUserAvatar.load(this.avatar) {
placeholder(R.mipmap.clipart_default_avatar_round)
error(R.mipmap.clipart_default_avatar_round)
transformations(RoundedCornersTransformation(36f))
}
it.name = this.name
it.switchAccessory.isSelected = this.list[position].isStar
it.switchAccessory.setOnClickListener(clickListener)
}
}

override fun getLayout(): Int {
return R.layout.recycler_groupie_item_star_user
}

override fun initializeViewBinding(view: View): RecyclerGroupieItemStarUserBinding {
return DataBindingUtil.bind(view) ?: throw IllegalStateException("unable to bind view")
}
}
  • 每次调用会进入bind方法,所以传入一个List列表,根据列表中的isStar属性判断是否是星标用户
  • 头像不能直接显示,要通过load方法加载图片
    • placeholder:默认图片
    • error: 出现错误时的图片
    • transformations: 图片相关操作,这里定义了图片圆角
  • getLayout用于绑定到页面布局文件
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
<?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="name"
type="String" />

<variable
name="isStar"
type="Boolean" />

<import type="android.view.View" />
</data>

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@color/black">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:minHeight="56dp"
tools:background="@color/white">

<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/group_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="15dp"
android:layout_marginStart="16dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/switch_accessory"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_user_avatar"
android:layout_width="45dp"
android:layout_height="45dp"
android:focusable="true" />

<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_gravity="center"
android:alpha="1"
android:text="@{name}"
android:textSize="15sp"
tools:text="Subtitle Placeholder"
tools:visibility="visible" />

</androidx.appcompat.widget.LinearLayoutCompat>

<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switch_accessory"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="@drawable/ic_star_user"
android:clickable="@{true}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/group_title"
app:layout_constraintTop_toTopOf="parent" />

<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginHorizontal="16dp"
android:background="@color/app_std_separator_line"
app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

结果

Powered by Hexo & Theme Keep
Unique Visitor Page View