瀏覽代碼

feat: 首页音频播放

DoggyZhang 3 月之前
父節點
當前提交
9a819535bc
共有 27 個文件被更改,包括 631 次插入94 次删除
  1. 1 10
      app/src/main/java/com/adealink/weparty/commonui/widget/CommonButton.kt
  2. 27 24
      app/src/main/java/com/adealink/weparty/image/ImageConfig.kt
  3. 324 0
      app/src/main/java/com/adealink/weparty/module/playmate/widget/SoundView.kt
  4. 28 4
      app/src/main/java/com/adealink/weparty/module/profile/data/ProfileData.kt
  5. 1 1
      app/src/main/java/com/adealink/weparty/module/profile/viewmodel/IProfileViewModel.kt
  6. 17 13
      app/src/main/java/com/adealink/weparty/oss/OssService.kt
  7. 17 0
      app/src/main/java/com/adealink/weparty/util/FileUtil.kt
  8. 二進制
      app/src/main/res/drawable-xhdpi/common_playmate_sound_pause_ic.png
  9. 二進制
      app/src/main/res/drawable-xhdpi/common_playmate_sound_play_ic.png
  10. 10 0
      app/src/main/res/drawable/common_playmate_sound_widget_bg.xml
  11. 42 0
      app/src/main/res/layout/layout_sound_view.xml
  12. 4 0
      app/src/main/res/values/attrs.xml
  13. 1 0
      app/src/main/res/values/strings.xml
  14. 3 0
      frame/tuikit/TUIChat/tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/AudioPlayer.java
  15. 21 1
      module/playmate/src/main/java/com/adealink/weparty/playmate/list/adapter/PlaymateListItemViewBinder.kt
  16. 5 5
      module/playmate/src/main/res/layout/item_playmate_home_list.xml
  17. 3 0
      module/profile/src/main/java/com/adealink/weparty/profile/comp/ProfileHeaderComp.kt
  18. 22 0
      module/profile/src/main/java/com/adealink/weparty/profile/data/Constants.kt
  19. 1 1
      module/profile/src/main/java/com/adealink/weparty/profile/data/ProfileData.kt
  20. 1 1
      module/profile/src/main/java/com/adealink/weparty/profile/edit/EditProfileActivity.kt
  21. 5 7
      module/profile/src/main/java/com/adealink/weparty/profile/search/adapter/SearchItemViewBinder.kt
  22. 40 4
      module/profile/src/main/java/com/adealink/weparty/profile/viewmodel/ProfileViewModel.kt
  23. 35 18
      module/profile/src/main/res/layout/activity_edit_profile.xml
  24. 1 1
      module/profile/src/main/res/layout/layout_me_function.xml
  25. 21 2
      module/profile/src/main/res/layout/layout_user_profile_header.xml
  26. 0 1
      module/profile/src/main/res/values/strings.xml
  27. 1 1
      module/setting/src/main/res/layout/activity_setting.xml

+ 1 - 10
app/src/main/java/com/adealink/weparty/commonui/widget/CommonButton.kt

@@ -115,17 +115,8 @@ class CommonButton @JvmOverloads constructor(
                     addState(
                         intArrayOf(-android.R.attr.state_enabled),
                         DrawableBuilder()
-                            .gradient(true)
-                            .orientation(FlipDrawable.ORIENTATION_HORIZONTAL)
-                            .gradientRadius(buttonRadius)
                             .cornerRadius(buttonRadius.toInt())
-                            .gradientColors(
-                                getCompatColor(R.color.color_FFB0F5EA),
-                                getCompatColor(R.color.color_FFA3CDF9),
-                                null
-                            )
-                            .gradientType(GradientDrawable.LINEAR_GRADIENT)
-                            .angle(0)
+                            .solidColor(getCompatColor(R.color.color_FFC9CDD4))
                             .build()
                     )
                     addState(

+ 27 - 24
app/src/main/java/com/adealink/weparty/image/ImageConfig.kt

@@ -49,29 +49,30 @@ class ImageConfig : IImageConfig, IGlobalConfigListener {
     }
 
     //TODO 全量CDN之后以下部分需要被移除,减少判断性能消耗
-    private val aliOssHosts = hashSetOf(
-        "yoki-resource-cf.wenext.media",
-        "yoki-user-cf.wenext.media",
-        "wenext-yoki-user.oss-ap-southeast-1.aliyuncs.com",
-
-        "wenext-lama.oss-ap-southeast-1.aliyuncs.com",
-        "wenext-lama.oss-accelerate.aliyuncs.com",
-        "wenext-lama.oss-accelerate-overseas.aliyuncs.com",
-        "wyak.oss-ap-southeast-1.aliyuncs.com",
-        "wyak.oss-accelerate.aliyuncs.com",
-        "wyak.oss-accelerate-overseas.aliyuncs.com",
-
-        "wenext-yoki.oss-ap-southeast-1.aliyuncs.com",
-        "wenext-yoki.oss-accelerate.aliyuncs.com",
-        "wenext-yoki.oss-accelerate-overseas.aliyuncs.com",
+    private val aliOssHosts = emptySet<String>(
+//        "yoki-resource-cf.wenext.media",
+//        "yoki-user-cf.wenext.media",
+//        "wenext-yoki-user.oss-ap-southeast-1.aliyuncs.com",
+//
+//        "wenext-lama.oss-ap-southeast-1.aliyuncs.com",
+//        "wenext-lama.oss-accelerate.aliyuncs.com",
+//        "wenext-lama.oss-accelerate-overseas.aliyuncs.com",
+//        "wyak.oss-ap-southeast-1.aliyuncs.com",
+//        "wyak.oss-accelerate.aliyuncs.com",
+//        "wyak.oss-accelerate-overseas.aliyuncs.com",
+//
+//        "wenext-yoki.oss-ap-southeast-1.aliyuncs.com",
+//        "wenext-yoki.oss-accelerate.aliyuncs.com",
+//        "wenext-yoki.oss-accelerate-overseas.aliyuncs.com",
     )
 
     //TODO 全量CDN之后以下部分需要被移除,减少判断性能消耗
     private val qCloudCosHosts = hashSetOf(
-        "wayak-system-1314119829.cos.eu-frankfurt.myqcloud.com",
-        "wayak-system-1314119829.cos.accelerate.myqcloud.com",
-        "weparty-user-1314119829.cos.eu-frankfurt.myqcloud.com",
-        "weparty-user-1314119829.cos.accelerate.myqcloud.com"
+        "lanu-public-test-1377959011.cos.ap-guangzhou.myqcloud.com",
+//        "wayak-system-1314119829.cos.eu-frankfurt.myqcloud.com",
+//        "wayak-system-1314119829.cos.accelerate.myqcloud.com",
+//        "weparty-user-1314119829.cos.eu-frankfurt.myqcloud.com",
+//        "weparty-user-1314119829.cos.accelerate.myqcloud.com"
     )
 
     //阿里云cdn回源到阿里oss
@@ -80,15 +81,17 @@ class ImageConfig : IImageConfig, IGlobalConfigListener {
     )
 
     //腾讯云cdn回源到阿里oss
-    private val qCloudCdn2AliOssHosts = hashSetOf(
-        "lama.wenext.media",
-        "wyak-user.wenext.media"
+    private val qCloudCdn2AliOssHosts = emptyList<String>(
+//        "public.gami.vip",
+//        "lama.wenext.media",
+//        "wyak-user.wenext.media"
     )
 
     //腾讯云cdn回源到腾讯云cos
     private val qCloudCdn2QCloudCosHosts = hashSetOf(
-        "wayak-resource.wenext.media",
-        "weparty-user.wenext.media"
+        "public.gami.vip",
+//        "wayak-resource.wenext.media",
+//        "weparty-user.wenext.media"
     )
 
     init {

+ 324 - 0
app/src/main/java/com/adealink/weparty/module/playmate/widget/SoundView.kt

@@ -0,0 +1,324 @@
+package com.adealink.weparty.module.playmate.widget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.media.MediaPlayer
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.animation.Animation
+import android.view.animation.LinearInterpolator
+import android.view.animation.RotateAnimation
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.withStyledAttributes
+import com.adealink.frame.base.IError
+import com.adealink.frame.download.listener.TaskListener
+import com.adealink.frame.download.task.Task
+import com.adealink.frame.log.Log
+import com.adealink.frame.oss.data.UploadFile
+import com.adealink.frame.storageService
+import com.adealink.frame.util.md5
+import com.adealink.frame.util.onClick
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.App
+import com.adealink.weparty.R
+import com.adealink.weparty.databinding.LayoutSoundViewBinding
+import com.adealink.weparty.storage.file.FilePath.audioPath
+import com.adealink.weparty.util.getFilePathBy
+import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioPlayer
+import kotlin.math.max
+
+@SuppressLint("SetTextI18n")
+class SoundView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+    companion object {
+        const val TAG = "SoundView"
+
+        const val MSG_UPDATE_DURATION = 1
+
+        const val MSG_UPDATE_PLAY_DURATION = 2
+    }
+
+    data class LoadSoundTask(
+        val url: String
+    )
+
+    private val binding = LayoutSoundViewBinding.inflate(LayoutInflater.from(context), this)
+
+    private var durationTextSize = 12f
+
+    private var soundUrl: String? = null
+    private var soundPath: String? = null
+    private var soundDuration: Long? = null
+
+    private var loadingAnimation: Animation? = null
+
+    private val handler = object : Handler(Looper.getMainLooper()) {
+        override fun handleMessage(msg: Message) {
+            super.handleMessage(msg)
+            when (msg.what) {
+                MSG_UPDATE_DURATION -> {
+                    binding.tvDuration.text = "${max(((soundDuration ?: 0) / 1000).toInt(), 1)}\""
+                }
+
+                MSG_UPDATE_PLAY_DURATION -> {
+                    binding.tvDuration.text =
+                        "${max(((AudioPlayer.getInstance().playPosition ?: 0) / 1000), 1)}\""
+                    if (AudioPlayer.getInstance().isPlaying && AudioPlayer.getInstance().path == soundPath) {
+                        sendEmptyMessageDelayed(MSG_UPDATE_PLAY_DURATION, 1000L)
+                    } else {
+                        sendEmptyMessage(MSG_UPDATE_DURATION)
+                    }
+                }
+            }
+        }
+    }
+
+    init {
+        binding.root.setBackgroundResource(R.drawable.common_playmate_sound_widget_bg)
+        if (isInEditMode) {
+            binding.tvDuration.text = "1000\""
+        }
+
+        initAttr(context)
+        binding.root.onClick {
+            playOrStop()
+        }
+    }
+
+    private fun initAttr(context: Context, attrs: AttributeSet? = null) {
+        if (isInEditMode) {
+            return
+        }
+        context.withStyledAttributes(
+            attrs,
+            R.styleable.SoundView
+        ) {
+            durationTextSize = getFloat(
+                R.styleable.SoundView_sound_duration_text_size,
+                12f
+            )
+        }
+
+        binding.tvDuration.textSize = durationTextSize
+    }
+
+    fun setSoundUrl(url: String?) {
+        this.soundUrl = url
+        this.soundPath = null
+        this.soundDuration = null
+        stopPlay()
+        loadSound()
+    }
+
+    private fun loadSound() {
+        val url = soundUrl
+        if (url.isNullOrEmpty()) {
+            Log.e(TAG, "playSound fail, url is null")
+            return
+        }
+
+        downloadSound(
+            onFinish = { soundPath ->
+                Log.d(TAG, "loadSound, path:$soundPath")
+                val loadSoundTask = LoadSoundTask(url)
+                MediaPlayer().apply {
+                    try {
+                        setDataSource(soundPath)
+
+                        // 设置准备完成监听器
+                        setOnPreparedListener { mp ->
+                            Log.e(TAG, "loadSound, onPrepared")
+                            if (loadSoundTask.url == soundUrl) {
+                                soundDuration = mp.duration.toLong()
+                                Log.e(TAG, "loadSound, url:$soundUrl, duration:$soundDuration")
+                                handler.sendEmptyMessage(MSG_UPDATE_DURATION)
+                            }
+
+                            //释放
+                            release()
+                        }
+
+                        // 设置错误监听器
+                        setOnErrorListener { _, what, extra ->
+                            Log.e(TAG, "loadSound error")
+                            if (loadSoundTask.url == soundUrl) {
+                                soundDuration = 0
+                                handler.sendEmptyMessage(MSG_UPDATE_DURATION)
+                            }
+                            false
+                        }
+
+                        setOnCompletionListener {
+                            release()
+                        }
+
+                        // 异步准备音频
+                        prepareAsync()
+                    } catch (e: Exception) {
+                        Log.d(TAG, "loadSound fail, for ${e.message}", e)
+                        release()
+                    }
+                }
+            }
+        )
+    }
+
+    private fun downloadSound(
+        onStartDownload: (() -> Unit)? = null,
+        onDownloadProgress: (() -> Unit)? = null,
+        onFinish: ((soundPath: String) -> Unit)? = null,
+        onError: (() -> Unit)? = null
+    ) {
+        val url = soundUrl
+        if (url.isNullOrEmpty()) {
+            Log.e(TAG, "downloadSound fail, url is null")
+            return
+        }
+        val soundPath = url.getFilePathBy(audioPath, UploadFile.FileType.MP3)
+        if (soundPath.isNullOrEmpty()) {
+            Log.e(TAG, "downloadSound fail, can not getFilePath for $url")
+            return
+        }
+        this.soundPath = soundPath
+        val soundFile = storageService.file.createWeFile(soundPath)
+        if (soundFile.exists()) {
+            Log.d(TAG, "downloadSound return, for sound exists, path:$soundPath")
+            onFinish?.invoke(soundPath)
+            return
+        }
+        Log.d(TAG, "downloadSound, url:$url, path:$soundPath")
+        val taskId = url.md5()
+        val task = Task(taskId, url, soundPath)
+        task.listeners.add(object : TaskListener {
+            override fun onFinished(task: Task) {
+                if (isAttachedToWindow && task.path == url) {
+                    runOnUiThread {
+                        onStartDownload?.invoke()
+                    }
+                }
+            }
+
+            override fun onProgress(
+                task: Task,
+                progress: Int?,
+                currentSize: Long,
+                totalSize: Long
+            ) {
+                if (isAttachedToWindow && task.path == url) {
+                    runOnUiThread {
+                        onDownloadProgress?.invoke()
+                    }
+                }
+            }
+
+            override fun onError(task: Task, error: IError) {
+                if (isAttachedToWindow && task.path == url) {
+                    runOnUiThread {
+                        onError?.invoke()
+                    }
+                }
+            }
+        })
+        Log.d(
+            TAG,
+            "downloadSound, start download, url:$url, task:$task"
+        )
+        App.instance.downloadService.addTask(task)
+    }
+
+    fun playOrStop() {
+        downloadSound(
+            onFinish = { soundPath ->
+                if (AudioPlayer.getInstance().isPlaying && AudioPlayer.getInstance().path == soundPath) {
+                    stopPlay()
+                } else {
+                    startPlay(soundPath)
+                }
+            },
+            onStartDownload = {
+                playLoadingAnim()
+            },
+            onDownloadProgress = {
+                playLoadingAnim()
+            },
+            onError = {
+                stopLoadingAnim()
+            }
+        )
+    }
+
+    private fun playLoadingAnim() {
+        binding.btnPlay.setImageResource(R.drawable.common_playmate_sound_play_ic)
+        if (loadingAnimation != null) {
+            return
+        }
+        loadingAnimation?.cancel()
+        loadingAnimation = RotateAnimation(
+            0f,
+            360f,
+            Animation.RELATIVE_TO_SELF,
+            0.5f,
+            Animation.RELATIVE_TO_SELF,
+            0.5f
+        ).apply {
+            duration = 1000
+            repeatMode = Animation.RESTART
+            repeatCount = Animation.INFINITE
+            interpolator = LinearInterpolator()
+        }
+        binding.btnPlay.animation = loadingAnimation
+        loadingAnimation?.start()
+    }
+
+    private fun stopLoadingAnim() {
+        loadingAnimation?.cancel()
+        loadingAnimation = null
+    }
+
+    private fun startPlay(soundPath: String) {
+        AudioPlayer.getInstance().stopPlay()
+        AudioPlayer.getInstance().startPlay(soundPath, object : AudioPlayer.Callback {
+            override fun onCompletion(success: Boolean?) {
+                if (isAttachedToWindow && soundPath == this@SoundView.soundPath) {
+                    runOnUiThread {
+                        onStopPlay()
+                    }
+                }
+            }
+        })
+        onStartPlay()
+    }
+
+    fun stopPlay() {
+        AudioPlayer.getInstance().stopPlay()
+        onStopPlay()
+    }
+
+    private fun onStartPlay() {
+        binding.btnPlay.setImageResource(R.drawable.common_playmate_sound_pause_ic)
+        handler.sendEmptyMessage(MSG_UPDATE_PLAY_DURATION)
+    }
+
+    private fun onStopPlay() {
+        stopLoadingAnim()
+        binding.btnPlay.setImageResource(R.drawable.common_playmate_sound_play_ic)
+        handler.sendEmptyMessage(MSG_UPDATE_DURATION)
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        stopPlay()
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        stopPlay()
+    }
+
+
+}

+ 28 - 4
app/src/main/java/com/adealink/weparty/module/profile/data/ProfileData.kt

@@ -2,6 +2,7 @@ package com.adealink.weparty.module.profile.data
 
 import android.os.Parcelable
 import com.google.gson.annotations.GsonNullable
+import com.google.gson.annotations.Must
 import com.google.gson.annotations.SerializedName
 import kotlinx.parcelize.Parcelize
 
@@ -42,17 +43,28 @@ data class UserInfo(
     @GsonNullable
     @SerializedName("age") var age: Int? = null,
     @GsonNullable
+    @SerializedName("area") var area: String? = null,
+    @GsonNullable
+    @SerializedName("cover") var cover: String? = null,
+    @GsonNullable
     @SerializedName("gender") var gender: Int? = null,
     @GsonNullable
     @SerializedName("intro") var intro: String? = null, //用户简介
     @GsonNullable
     @SerializedName("photos") var photos: List<String>? = null, //照片
-    @GsonNullable
-    @SerializedName("playmate") var playmate: Boolean? = null, //是否陪玩师
 
 
     @GsonNullable
-    @SerializedName("fans") var fans: Int? = null, //是否陪玩师
+    @SerializedName("playmate") var playmate: Boolean? = null, //是否陪玩师
+    @GsonNullable
+    @SerializedName("languageNames") var languageNames: List<String>? = null, //用户会的语言列表
+    @GsonNullable
+    @SerializedName("star") var star: Float? = null, //评分
+    @GsonNullable
+    @SerializedName("skills") var skills: List<UserPlayMateSkill>? = null, //技能商品简易信息
+    @GsonNullable
+    @SerializedName("rated") var rated: Boolean? = null, //今天是否已打分
+
 ) : Parcelable {
 
     fun isComplete(): Boolean {
@@ -90,4 +102,16 @@ data class UserInfo(
 infix fun UserInfo.contentsTheSame(other: UserInfo?): Boolean {
     other ?: return false
     return this == other
-}
+}
+
+@Parcelize
+data class UserPlayMateSkill(
+    @Must
+    @SerializedName("id") var id: String, //技能商品id
+    @SerializedName("name") var name: String, //技能商品品类名称
+    @SerializedName("icon") var icon: String, //图标
+    @SerializedName("price") var price: Float, //价格
+    @SerializedName("unit") var unit: String, //单位
+    @SerializedName("cover") var cover: String, //封面
+    @SerializedName("orderCount") var orderCount: Long, //订单数量
+) : Parcelable

+ 1 - 1
app/src/main/java/com/adealink/weparty/module/profile/viewmodel/IProfileViewModel.kt

@@ -18,6 +18,6 @@ interface IProfileViewModel {
         newAvatarPath: String? = null,
         newNickName: String? = null,
         newGender: Int? = null,
-        newAge: String? = null,
+        newBirthday: String? = null,
     ): LiveData<Rlt<Any>>
 }

+ 17 - 13
app/src/main/java/com/adealink/weparty/oss/OssService.kt

@@ -115,19 +115,23 @@ object OssService : IOssService {
                 return@withContext Rlt.Failed(CommonDataNullError())
             }
 
-            val uploadCall = App.instance.networkService.fileHttpClient
-                .newCall(
-                    Request.Builder()
-                        .url(uploadOssRes.uploadUrl)
-                        .method(("put").uppercase(), getUploadBody(file))
-                        .build()
-                )
-            val response = uploadCall.execute()
-            if (response.isSuccessful) {
-                Rlt.Success(uploadOssRes.fileUrl)
-            } else {
-                val result = response.body?.string()
-                Rlt.Failed(OSSUploadError(result ?: ""))
+            try {
+                val uploadCall = App.instance.networkService.fileHttpClient
+                    .newCall(
+                        Request.Builder()
+                            .url(uploadOssRes.uploadUrl)
+                            .method(("put").uppercase(), getUploadBody(file))
+                            .build()
+                    )
+                val response = uploadCall.execute()
+                if (response.isSuccessful) {
+                    Rlt.Success(uploadOssRes.fileUrl)
+                } else {
+                    val result = response.body?.string()
+                    Rlt.Failed(OSSUploadError(result ?: ""))
+                }
+            } catch (e: Exception) {
+                Rlt.Failed(OSSUploadError(e.message ?: ""))
             }
         }
     }

+ 17 - 0
app/src/main/java/com/adealink/weparty/util/FileUtil.kt

@@ -0,0 +1,17 @@
+package com.adealink.weparty.util
+
+import com.adealink.frame.oss.data.UploadFile
+import com.adealink.frame.storage.file.imageProgressPath
+import com.adealink.frame.util.getFileSuffix
+import com.adealink.frame.util.md5
+import java.io.File
+
+fun String?.getFilePathBy(path: String, fileType: UploadFile.FileType): String? {
+    if (isNullOrEmpty()) {
+        return null
+    }
+    return File(
+        imageProgressPath,
+        "${md5()}.${fileType.suffix.ifEmpty { getFileSuffix(this) }}"
+    ).path
+}

二進制
app/src/main/res/drawable-xhdpi/common_playmate_sound_pause_ic.png


二進制
app/src/main/res/drawable-xhdpi/common_playmate_sound_play_ic.png


+ 10 - 0
app/src/main/res/drawable/common_playmate_sound_widget_bg.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="180"
+        android:endColor="#4ED2FF"
+        android:startColor="#B1EF5D"
+        android:type="linear" />
+    <corners android:radius="100dp" />
+</shape>

+ 42 - 0
app/src/main/res/layout/layout_sound_view.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge 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:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@drawable/common_playmate_sound_widget_bg"
+    tools:layout_height="30dp"
+    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/btn_play"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="3dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintDimensionRatio="1:1"
+        app:layout_constraintEnd_toStartOf="@id/tv_price"
+        app:layout_constraintHeight_percent="0.84"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/common_playmate_sound_pause_ic" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_duration"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="6dp"
+        android:layout_marginEnd="12dp"
+        android:includeFontPadding="false"
+        android:textColor="@color/white"
+        android:textSize="12sp"
+        app:fontFamily="@font/poppins_semibold"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/btn_play"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="1000" />
+
+
+</merge>

+ 4 - 0
app/src/main/res/values/attrs.xml

@@ -599,6 +599,10 @@
         <attr name="unit_text_size" format="float" />
     </declare-styleable>
 
+    <declare-styleable name="SoundView">
+        <attr name="sound_duration_text_size" format="dimension" />
+    </declare-styleable>
+
     <declare-styleable name="SoundWaveView">
         <attr name="barWidth" format="dimension" />
         <attr name="barSpacing" format="dimension" />

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -266,4 +266,5 @@
     <string name="common_just_now">刚刚</string>
     <string name="common_help_center">Help Center</string>
     <string name="web_view_page_load_error_tip">The page failed to load, please try again</string>
+    <string name="common_settings">Settings</string>
 </resources>

+ 3 - 0
frame/tuikit/TUIChat/tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/AudioPlayer.java

@@ -29,6 +29,9 @@ public class AudioPlayer {
     }
 
     public void startPlay(String filePath, Callback callback) {
+        //执行一次stop
+        stopPlay();
+
         mAudioPath = filePath;
         mPlayCallback = callback;
         setSpeakerMode();

+ 21 - 1
module/playmate/src/main/java/com/adealink/weparty/playmate/list/adapter/PlaymateListItemViewBinder.kt

@@ -4,6 +4,7 @@ import android.view.LayoutInflater
 import android.view.ViewGroup
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.ext.hide
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
@@ -26,13 +27,22 @@ class PlaymateListItemViewBinder(val listener: OnPlaymateListListener) :
         }
 
         holder.binding.ivAvatar.setImageUrl(item.data.avatar)
+        if (item.data.voice.isEmpty()) {
+            holder.binding.vVoice.gone()
+            holder.binding.vVoice.setSoundUrl(null)
+        } else {
+            holder.binding.vVoice.show()
+            holder.binding.vVoice.setSoundUrl(item.data.voice)
+        }
+
 
         holder.binding.tvName.text = item.data.nickname
         holder.binding.vGender.setAge(item.data.age)
         holder.binding.vGender.setGender(item.data.gender)
         holder.binding.tvLocation.text = item.data.area
         holder.binding.tvStar.text = formatStar(item.data.star)
-        holder.binding.tvComment.text = getCompatString(R.string.playmate_comment_count, item.data.commentCount.toString())
+        holder.binding.tvComment.text =
+            getCompatString(R.string.playmate_comment_count, item.data.commentCount.toString())
         holder.binding.ivCategory.setImageUrl(item.data.categoryIcon)
         holder.binding.tvCategory.text = item.data.categoryName
         holder.binding.tvSummary.text = item.data.summary
@@ -68,6 +78,16 @@ class PlaymateListItemViewBinder(val listener: OnPlaymateListListener) :
         return BindingViewHolder(ItemPlaymateHomeListBinding.inflate(inflater, parent, false))
     }
 
+    override fun onViewDetachedFromWindow(holder: BindingViewHolder<ItemPlaymateHomeListBinding>) {
+        super.onViewDetachedFromWindow(holder)
+        holder.binding.vVoice.stopPlay()
+    }
+
+    override fun onViewRecycled(holder: BindingViewHolder<ItemPlaymateHomeListBinding>) {
+        holder.binding.vVoice.stopPlay()
+        super.onViewRecycled(holder)
+    }
+
     interface OnPlaymateListListener {
         fun goCategoryDetail(data: PlaymateListData)
     }

+ 5 - 5
module/playmate/src/main/res/layout/item_playmate_home_list.xml

@@ -14,10 +14,10 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_voice_summary"
-        android:layout_width="64dp"
-        android:layout_height="22dp"
+    <com.adealink.weparty.module.playmate.widget.SoundView
+        android:id="@+id/v_voice"
+        android:layout_width="wrap_content"
+        android:layout_height="26dp"
         app:layout_constraintBottom_toBottomOf="@id/iv_avatar"
         app:layout_constraintEnd_toEndOf="@id/iv_avatar"
         app:layout_constraintStart_toStartOf="@id/iv_avatar"
@@ -28,7 +28,7 @@
         style="@style/CommonPriceView"
         app:layout_constraintEnd_toEndOf="@id/iv_avatar"
         app:layout_constraintStart_toStartOf="@id/iv_avatar"
-        app:layout_constraintTop_toBottomOf="@id/iv_voice_summary" />
+        app:layout_constraintTop_toBottomOf="@id/v_voice" />
 
     <androidx.constraintlayout.widget.Barrier
         android:id="@+id/v_barrier_start"

+ 3 - 0
module/profile/src/main/java/com/adealink/weparty/profile/comp/ProfileHeaderComp.kt

@@ -27,8 +27,11 @@ class ProfileHeaderComp(
 
     @SuppressLint("SetTextI18n")
     override fun updateUI(userInfo: UserInfo?) {
+        binding.ivBg.setImageUrl(userInfo?.cover)
         binding.tvName.text = userInfo?.nickName
         binding.tvUserId.text = "ID ${userInfo?.uid}"
+
+        binding.vSound.setSoundUrl("")
     }
 
     private fun showEvaluateDialog() {

+ 22 - 0
module/profile/src/main/java/com/adealink/weparty/profile/data/Constants.kt

@@ -0,0 +1,22 @@
+package com.adealink.weparty.profile.data
+
+const val PROFILE_GIFT_WALL_SPAN_COUNT = 4
+const val TAG_PROFILE = "tag_profile"
+const val TAG_PROFILE_ITEM_TOUCH = "tag_profile_item_touch"
+const val PROFILE_PHOTO_WALL_MAX_COUNT = 9
+const val PROFILE_COVER_MAX_COUNT = 6
+const val SOURCE_EDIT_AVATAR = "1"
+const val SOURCE_EDIT_PHOTO_WALL = "2"
+const val SOURCE_EDIT_PROFILE_COVER = "3"
+const val MAX_BIO_LENGTH = 280
+const val MAX_NAME_LENGTH = 28
+
+const val AVATAR_IMAGE_MAX_WIDTH = 300
+const val AVATAR_IMAGE_MAX_HEIGHT = 300
+const val AVATAR_IMAGE_MAX_SIZE_KB = 100
+const val AVATAR_IMAGE_MIN_QUALITY = 50
+
+const val PHOTO_WALL_IMAGE_MAX_WIDTH = 750
+const val PHOTO_WALL_IMAGE_MAX_HEIGHT = 1334
+const val PHOTO_WALL_IMAGE_MAX_SIZE_KB = 500
+const val PHOTO_WALL_IMAGE_MIN_QUALITY = 50

+ 1 - 1
module/profile/src/main/java/com/adealink/weparty/profile/data/ProfileData.kt

@@ -20,5 +20,5 @@ data class UpdateMyUserInfoReq(
     @SerializedName("avatar") val avatar: String? = null,
     @SerializedName("nickname") val nickname: String? = null,
     @SerializedName("gender") val gender: Int? = null,
-    @SerializedName("age") val age: Int? = null,
+    @SerializedName("birthday") val birthday: String? = null,
 )

+ 1 - 1
module/profile/src/main/java/com/adealink/weparty/profile/edit/EditProfileActivity.kt

@@ -112,7 +112,7 @@ class EditProfileActivity : BaseActivity() {
             newAvatarPath = avatarComp.getAvatarPath(),
             newNickName = binding.etUserName.text?.toString(),
             newGender = inputGender?.gender,
-            newAge = binding.etUserAge.text?.toString(),
+            newBirthday = binding.etUserAge.text?.toString(),
         ).observe(this) { rlt ->
             dismissLoading()
             when (rlt) {

+ 5 - 7
module/profile/src/main/java/com/adealink/weparty/profile/search/adapter/SearchItemViewBinder.kt

@@ -3,11 +3,9 @@ package com.adealink.weparty.profile.search.adapter
 import android.annotation.SuppressLint
 import android.view.LayoutInflater
 import android.view.ViewGroup
-import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.util.onClick
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
-import com.adealink.weparty.profile.R
 import com.adealink.weparty.profile.databinding.LayoutProfileSearchItemBinding
 import com.adealink.weparty.profile.search.data.SearchResultItemData
 
@@ -27,11 +25,11 @@ class SearchItemViewBinder(val listener: OnResultClickListener) :
         holder.binding.vGender.setGender(item.data.userinfo.gender)
         holder.binding.vGender.setAge(item.data.userinfo.age)
         holder.binding.tvId.text = "ID ${item.data.userinfo.uid}"
-        holder.binding.tvFans.text =
-            getCompatString(
-                R.string.profile_search_user_fans,
-                (item.data.userinfo.fans ?: 0).toString()
-            )
+//        holder.binding.tvFans.text =
+//            getCompatString(
+//                R.string.profile_search_user_fans,
+//                (item.data.userinfo.fans ?: 0).toString()
+//            )
     }
 
     override fun onCreateViewHolder(

+ 40 - 4
module/profile/src/main/java/com/adealink/weparty/profile/viewmodel/ProfileViewModel.kt

@@ -6,14 +6,22 @@ import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.base.CommonDataNullError
 import com.adealink.frame.base.CommonParamError
 import com.adealink.frame.base.Rlt
+import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.livedata.OnceMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
 import com.adealink.frame.oss.data.UploadFile
 import com.adealink.weparty.App
+import com.adealink.weparty.commonui.ext.isGIFImage
+import com.adealink.weparty.imageselect.util.compressImage
 import com.adealink.weparty.module.profile.data.UserInfo
 import com.adealink.weparty.module.profile.listener.IProfileListener
 import com.adealink.weparty.module.profile.viewmodel.IProfileViewModel
 import com.adealink.weparty.profile.R
+import com.adealink.weparty.profile.data.AVATAR_IMAGE_MAX_HEIGHT
+import com.adealink.weparty.profile.data.AVATAR_IMAGE_MAX_SIZE_KB
+import com.adealink.weparty.profile.data.AVATAR_IMAGE_MAX_WIDTH
+import com.adealink.weparty.profile.data.AVATAR_IMAGE_MIN_QUALITY
+import com.adealink.weparty.profile.data.TAG_PROFILE
 import com.adealink.weparty.profile.data.UpdateMyUserInfoReq
 import com.adealink.weparty.profile.datasource.remote.ProfileHttpService
 import com.adealink.weparty.profile.manager.profileManager
@@ -61,7 +69,7 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener {
         newAvatarPath: String?,
         newNickName: String?,
         newGender: Int?,
-        newAge: String?,
+        newBirthday: String?,
     ): LiveData<Rlt<Any>> {
         val liveData = OnceMutableLiveData<Rlt<Any>>()
         viewModelScope.launch {
@@ -85,7 +93,7 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener {
                 editUserInfo.gender = newGender
             }
 
-            newAge?.trim()?.let { newAge ->
+            newBirthday?.trim()?.let { newAge ->
                 if (newAge.isEmpty()) {
                     liveData.send(Rlt.Failed(CommonParamError(getCompatString(R.string.toast_profile_edit_age_empty))))
                     return@launch
@@ -120,7 +128,7 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener {
                     avatar = editUserInfo.avatar,
                     nickname = editUserInfo.nickName,
                     gender = editUserInfo.gender,
-                    age = editUserInfo.age
+//                    birthday = editUserInfo.age
                 )
             )
             liveData.send(rlt)
@@ -129,8 +137,36 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener {
     }
 
     private suspend fun uploadAvatar(avatarPath: String): Rlt<String> {
+        val compressFilePath = if (isGIFImage(avatarPath)) {
+            avatarPath
+        } else {
+            val compressRlt =
+                compressImage(
+                    avatarPath,
+                    AVATAR_IMAGE_MAX_WIDTH,
+                    AVATAR_IMAGE_MAX_HEIGHT,
+                    AVATAR_IMAGE_MAX_SIZE_KB,
+                    AVATAR_IMAGE_MIN_QUALITY
+                )
+
+            if (compressRlt is Rlt.Failed) {
+                Log.logRltD(
+                    TAG_PROFILE,
+                    "uploadAvatar, compress failed, filePath:${avatarPath}",
+                    compressRlt
+                )
+                return compressRlt
+            }
+            val compressResult = (compressRlt as Rlt.Success).data
+            compressResult.path
+        }
         return App.instance.ossService.suspendUploadFile(
-            UploadFile.LocalFile(avatarPath, UploadFile.FileType.IMAGE)
+            UploadFile.LocalFile(
+                compressFilePath,
+                UploadFile.FileType.IMAGE,
+                compress = false,
+                deleteAfterUploaded = avatarPath != compressFilePath
+            )
         )
     }
 }

+ 35 - 18
module/profile/src/main/res/layout/activity_edit_profile.xml

@@ -5,12 +5,44 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <com.adealink.weparty.commonui.widget.CommonTopBar
+    <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/top_bar"
         android:layout_width="match_parent"
         android:layout_height="@dimen/common_top_bar_height"
-        app:layout_constraintTop_toTopOf="parent"
-        app:top_bar_title="@string/profile_edit_title" />
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_close"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:rotationY="@integer/locale_mirror_flip"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/commonui_back_black_48_ic" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/top_bar_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/profile_edit_title"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <com.adealink.weparty.commonui.widget.CommonButton
+            android:id="@+id/btn_save"
+            android:layout_width="wrap_content"
+            android:layout_height="32dp"
+            android:minWidth="66dp"
+            android:paddingHorizontal="8dp"
+            android:text="@string/common_save"
+            app:layout_constraintBottom_toBottomOf="@id/top_bar"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="@id/top_bar" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
     <androidx.core.widget.NestedScrollView
         android:layout_width="match_parent"
@@ -171,21 +203,6 @@
 
             </androidx.constraintlayout.widget.ConstraintLayout>
 
-            <com.adealink.weparty.commonui.widget.CommonButton
-                android:id="@+id/btn_save"
-                android:layout_width="0dp"
-                android:layout_height="48dp"
-                android:layout_marginTop="24dp"
-                android:fontFamily="@font/poppins_semibold"
-                android:gravity="center"
-                android:includeFontPadding="false"
-                android:text="@string/common_save"
-                android:textColor="@color/white"
-                android:textSize="16sp"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/cl_content" />
-
         </androidx.constraintlayout.widget.ConstraintLayout>
 
     </androidx.core.widget.NestedScrollView>

+ 1 - 1
module/profile/src/main/res/layout/layout_me_function.xml

@@ -124,7 +124,7 @@
             android:gravity="center"
             android:includeFontPadding="false"
             android:singleLine="true"
-            android:text="@string/profile_me_setting"
+            android:text="@string/common_settings"
             android:textColor="@color/color_FF4E5969"
             android:textSize="12sp"
             app:layout_constrainedWidth="true"

+ 21 - 2
module/profile/src/main/res/layout/layout_user_profile_header.xml

@@ -32,10 +32,19 @@
         app:layout_constraintBottom_toBottomOf="@id/iv_bg"
         app:layout_constraintStart_toStartOf="@id/iv_bg">
 
+        <com.adealink.weparty.module.playmate.widget.SoundView
+            android:id="@+id/v_sound"
+            android:layout_width="wrap_content"
+            android:layout_height="26dp"
+            android:layout_marginBottom="6dp"
+            app:layout_constraintBottom_toTopOf="@id/tv_name"
+            app:layout_constraintStart_toStartOf="parent" />
+
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/tv_name"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_marginBottom="2.5dp"
             android:ellipsize="end"
             android:fontFamily="@font/poppins_semibold"
             android:includeFontPadding="false"
@@ -63,14 +72,24 @@
             android:id="@+id/tv_user_id"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:fontFamily="@font/poppins_semibold"
+            android:layout_marginBottom="2.5dp"
             android:includeFontPadding="false"
             android:textColor="@color/color_FFC9CDD4"
             android:textSize="12sp"
-            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/tv_fans"
             app:layout_constraintStart_toStartOf="parent"
             tools:text="ID 12345678" />
 
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_fans"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FFC9CDD4"
+            android:textSize="12sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            tools:text="12.3k 粉丝 10k获赞" />
 
         <androidx.constraintlayout.widget.Barrier
             android:id="@+id/v_barrier_end"

+ 0 - 1
module/profile/src/main/res/values/strings.xml

@@ -33,5 +33,4 @@
     <string name="profile_me_follower">Follower</string>
     <string name="profile_me_fans">Fans</string>
     <string name="profile_me_function_center">Function Center</string>
-    <string name="profile_me_setting">Settings</string>
 </resources>

+ 1 - 1
module/setting/src/main/res/layout/activity_setting.xml

@@ -9,7 +9,7 @@
         android:layout_width="match_parent"
         android:layout_height="@dimen/common_top_bar_height"
         app:layout_constraintTop_toTopOf="parent"
-        app:top_bar_title="@string/common_help_center" />
+        app:top_bar_title="@string/common_settings" />
 
     <androidx.core.widget.NestedScrollView
         android:layout_width="0dp"