14 Commits

Author SHA1 Message Date
d3bd838753 1:上线版本修改 2025-11-12 18:51:09 +08:00
02715f1346 新的上架提交 2025-11-12 17:32:26 +08:00
c1d77850f1 Merge branch 'dev-lxj-beta' into dev-lxj
# Conflicts:
#	.idea/.name
#	.idea/vcs.xml
2025-11-05 11:25:39 +08:00
773c3fc8b4 1:修改部分BUG 2025-11-05 11:24:32 +08:00
d0337a723c 1:修改部分BUG 2025-11-05 11:13:18 +08:00
96c679528e 1:修改交友房出现嘉宾会闭麦的情况 2025-11-03 20:34:42 +08:00
6b9490cceb 1:修改交友房出现嘉宾会闭麦的情况 2025-11-03 20:29:52 +08:00
877ef5ea05 停止跟踪 2025-11-03 18:38:48 +08:00
771138d34c 修改练歌房 2025-11-03 18:37:58 +08:00
520bc8e1bb 修改图片 2025-11-03 18:34:07 +08:00
f5377127ce 1:修改交友房出现嘉宾会闭麦的情况 2025-11-03 10:08:58 +08:00
f4f04b59a8 1:添加权限说明
2:聊天添加举报按钮
2025-10-30 18:19:16 +08:00
a993a7710f 1:添加权限说明
2:聊天添加举报按钮
2025-10-30 18:13:59 +08:00
a5ebc83dea 1:修改pk房
2:修改页面跳转
2025-10-30 09:07:35 +08:00
3733 changed files with 37175 additions and 94276 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.idea/
*.iml
build/
local.properties
.gradle/
.externalNativeBuild/
.DS_Store
*.dm
*.log

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-06T06:22:54.522558600Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=emulator-5554;connection=1b58820a" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

33
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$/yusheng-android" />
<option name="gradleJvm" value="#JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/yusheng-android" />
<option value="$PROJECT_DIR$/yusheng-android/Loadinglibrary" />
<option value="$PROJECT_DIR$/yusheng-android/app" />
<option value="$PROJECT_DIR$/yusheng-android/moduleLogin" />
<option value="$PROJECT_DIR$/yusheng-android/moduleUtil" />
<option value="$PROJECT_DIR$/yusheng-android/modulecircle" />
<option value="$PROJECT_DIR$/yusheng-android/modulemain" />
<option value="$PROJECT_DIR$/yusheng-android/modulenews" />
<option value="$PROJECT_DIR$/yusheng-android/moduleroom" />
<option value="$PROJECT_DIR$/yusheng-android/moduletablayout" />
<option value="$PROJECT_DIR$/yusheng-android/modulevocal" />
<option value="$PROJECT_DIR$/yusheng-android/modulevoice" />
<option value="$PROJECT_DIR$/yusheng-android/timcommon" />
<option value="$PROJECT_DIR$/yusheng-android/tuichat" />
<option value="$PROJECT_DIR$/yusheng-android/tuiconversation" />
<option value="$PROJECT_DIR$/yusheng-android/tuicore" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$/yusheng-android" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="ms-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/yusheng-android" vcs="Git" />
</component>
</project>

Binary file not shown.

View File

@@ -1,837 +0,0 @@
<!-- saved from url=(0051)https://vespa.qxyushen.top//api/Page/page_show?id=4 -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>.Po4BvhR1CK2tJaywJ6AN path {
fill: var(--icon-path-fill);
}
.Oz4yDjua3Qe6thqkZYf_ path {
transition: 0.2s all;
}
.Oz4yDjua3Qe6thqkZYf_:hover path {
fill: var(--icon-hover-fill);
}
</style>
<style>/* !!!切勿直接改动该文件,该文件由 generator.ts 自动生成!!! */
/* !!! DONT MODIFY THIS FILE DIRECTLY, THIS FILE IS GENERATED BY generator.ts AUTOMATICALLY !!! */
.ibW4Oa5B7s2zJKKZ4pCg {
user-select: none;
}
.AtqKyJetjrG4smuk35Np {
max-width: 346px;
width: auto;
height: 36px;
background-color: var(--quark-style-white-color, #fff);
padding-left: 10px;
padding-right: 4px;
display: flex;
align-items: center;
box-sizing: border-box;
border: 1px solid var(--quark-style-gray-20-color, rgba(6, 10, 38, 0.12));
box-shadow: 0 12px 32px -6px var(--quark-style-gray-30-fixed-color, rgba(6, 10, 38, 0.24));
border-radius: 10px;
}
.ibW4Oa5B7s2zJKKZ4pCg .g6iGsZa_KHMeW2yICzQQ {
height: 28px;
display: flex;
align-items: center;
margin-right: 6px;
}
.ibW4Oa5B7s2zJKKZ4pCg .e4UXx38MPgfHdym_Lzt0 {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
height: 28px;
padding: 0 6px;
margin-right: 2px;
border-radius: 6px;
column-gap: 4px;
}
.ibW4Oa5B7s2zJKKZ4pCg .e4UXx38MPgfHdym_Lzt0:hover:not(.ibW4Oa5B7s2zJKKZ4pCg .kNOcXLDT_cCrcoY8LTm8) {
background: var(--quark-style-gray-10-color, rgba(6, 10, 38, 0.06));
}
.ibW4Oa5B7s2zJKKZ4pCg .kNOcXLDT_cCrcoY8LTm8 {
cursor: default;
}
.ibW4Oa5B7s2zJKKZ4pCg .kNOcXLDT_cCrcoY8LTm8 .Va3czASiR9Zztyl_lD4M {
color: var(--quark-style-gray-40-color, rgba(6, 10, 38, 0.4)) !important;
}
.ibW4Oa5B7s2zJKKZ4pCg .e4UXx38MPgfHdym_Lzt0 .Va3czASiR9Zztyl_lD4M {
font-size: 12px;
color: var(--quark-style-gray-color, #060A26);
line-height: 16px;
white-space: nowrap;
position: relative;
}
.ibW4Oa5B7s2zJKKZ4pCg .llw0qsmiI_08u93bFdNg {
position: relative;
width: 28px;
height: 28px;
overflow: visible !important;
}
.ibW4Oa5B7s2zJKKZ4pCg .LEo8kpqIERehkv8AhAfG {
width: 28px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 6px;
overflow: visible !important;
}
.ibW4Oa5B7s2zJKKZ4pCg .LEo8kpqIERehkv8AhAfG:hover {
background: var(--quark-style-gray-10-color, rgba(6, 10, 38, 0.06));
}
.ibW4Oa5B7s2zJKKZ4pCg .zoNmooxAnbLEJSN8m1Jg {
box-sizing: border-box;
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
width: 110px;
max-height: 136px;
height: auto;
top: 36px;
right: -5px;
padding: 4px 0;
background-color: var(--quark-style-white-color, #fff);
border: 1px solid var(--quark-style-gray-20-color, rgba(6, 10, 38, 0.12));
box-shadow: 0 4px 16px -6px var(--quark-style-gray-20-fixed-color, rgba(6, 10, 38, 0.12));
border-radius: 8px;
overflow: visible !important;
row-gap: 4px;
}
.ibW4Oa5B7s2zJKKZ4pCg .O1imPofna4elG_8NcQnR {
top: -77px;
}
.ibW4Oa5B7s2zJKKZ4pCg .mdH0IY7jS3Swn5vdX6tz {
width: 102px;
height: 28px;
display: flex;
align-items: center;
justify-content: flex-start;
cursor: pointer;
column-gap: 8px;
border-radius: 6px;
padding: 0 6px;
box-sizing: border-box;
}
.ibW4Oa5B7s2zJKKZ4pCg .mdH0IY7jS3Swn5vdX6tz:hover:not(.ibW4Oa5B7s2zJKKZ4pCg .dEdHLVmn_L2GAzb_cmwt) {
background: var(--quark-style-gray-10-color, rgba(6, 10, 38, 0.06));
}
.ibW4Oa5B7s2zJKKZ4pCg .dEdHLVmn_L2GAzb_cmwt {
cursor: default;
}
.ibW4Oa5B7s2zJKKZ4pCg .dEdHLVmn_L2GAzb_cmwt .zEraruudgjR2MToGu4Kw {
color: var(--quark-style-gray-40-color, rgba(6, 10, 38, 0.4)) !important;
}
.ibW4Oa5B7s2zJKKZ4pCg .XfCMwvO0DsqFCeyzPYP2 {
width: 16px;
height: 16px;
}
.ibW4Oa5B7s2zJKKZ4pCg .zEraruudgjR2MToGu4Kw {
font-size: 12px;
color: var(--quark-style-gray-color, #060A26);
}
.ibW4Oa5B7s2zJKKZ4pCg .KZeoAuXbMIkWzOT4PcH5 {
width: 100%;
height: 1px;
background: var(--quark-style-gray-10-color, rgba(6, 10, 38, 0.06));
}
.ZL32C_XdLL8UQRZ3zObd {
display: flex;
align-items: center;
}
.u5llx7cIQZLdrjP5Vag1 {
width: 28px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 16px;
cursor: pointer;
margin-right: 12px;
background: var(--quark-style-gray-60-color, rgba(6, 10, 38, 0.6));
}
.ZL32C_XdLL8UQRZ3zObd .LEo8kpqIERehkv8AhAfG {
border-radius: 16px !important;
background: var(--quark-style-gray-60-color, rgba(6, 10, 38, 0.6)) !important;
}
.ZL32C_XdLL8UQRZ3zObd .zoNmooxAnbLEJSN8m1Jg {
right: 0 !important;
}
.ZL32C_XdLL8UQRZ3zObd {
overflow: visible !important;
}
</style>
</head>
<body><p><strong>羽声语音直播隐私保护政策</strong></p>
<p><strong>本版本更新日期2025年【10】月【27】日</strong></p>
<p><strong>本版本生效日期2025年【10】月【27】日</strong></p>
<p><strong>引言:</strong></p>
<p style="text-align: left;"><strong>“羽声语音”APP 的运营主体系陕西启星汇申网络科技有限公司(以下简称“我们”&amp;
”羽声”)深知个人信息对您的重要性,并会尽力保护您的个人信息安全可靠,我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则,目的明确原则,选择同意原则,最少够用原则,确保安全原则,主体参与原则,公开透明原则等。同时我们承诺,我们将按业界成熟的安全解决方案,采取相应的安全保护措施来保护您的个人信息。鉴此,我们制定本《隐私政策》(下称“本政策
/ 本隐私政策”)并提醒您: 本政策适用于羽声语音语音软件及相关服务。
需要特别说明的是,本政策不适用于其他第三方向您提供的服务,也不适用于已另行独立设置隐私权政策的产品或服务。
在使用羽声语音语音软件或服务前,请您务必仔细阅读并透彻理解本政策,特别是以粗体/粗体下划线标识的条款,您应重点阅读,在确认充分理解并同意后使用相关产品或服务。若您拒绝本政策,仅能进入访客态进行浏览。为保障软件及服务的安全运行,呱友在相应功能点触发时会收集相关设备信息及权限。如对本政策内容有任何疑问、意见或建议,您可通过呱友提供的各种联系方式与我们联系。
我们非常重视用户个人信息的保护,并且将以勤勉和审慎的义务对待这些信息。您在下载、安装、开启、浏览、注册、登录、使用(以下统称“使用”)本公司软件及相关服务时,本公司将按照本《隐私政策》收集、保存、使用、共享、披露及保护您的个人信息鉴此,我们希望通过本《隐私政策》向您介绍我们对您个人信息的处理方式并提醒您:
在使用“羽声”各项产品或服务前,请您留意重点阅读。若您认为本协议中的加粗条款可能会导致您的部分或全部权利或权益受损,请您务必再次仔细阅读,在确保您已经理解、接受了加粗条款的前提下,继续使用本公司服务。如对本政策内容有任何疑问、意见或建议,您可通过提供的各种联系方式与我们联系。</strong>
</p>
<p><strong>本政策将帮助您了解如下内容:</strong></p>
<p><strong>一、定义及适用范围</strong></p>
<p><strong>二、我们如何收集和使用您的个人信息</strong></p>
<p><strong>三、我们如何使用Cookie和第三方H5页面</strong></p>
<p><strong>四、我们如何共享、转让、公开披露您的个人信息</strong></p>
<p><strong>五、我们如何存储和保护您的个人信息</strong></p>
<p><strong>六、您如何管理您的个人信息</strong></p>
<p><strong>七、未成年人保护</strong></p>
<p><strong>八、本政策的更新</strong></p>
<p><strong>九、联系我们</strong></p>
<p><strong>十、其他</strong></p>
<p><strong>【特别提示】请您在使用我们各项产品或服务前,仔细阅读并透彻理解本政策,特别是以粗体/下划线/粗体下划线等有特殊标识的条款,您应重点阅读,在确认充分理解并同意后使用相关产品或服务。如果您不同意我们收集您的任何个人信息,您应当立即停止使用并退出羽声语音直播。当您使用或继续使用我们的产品或服务时,即表示您同意我们按照本政策来处理您的相关信息。</strong>
</p>
<p>
<strong>如对本政策内容有任何疑问,您可以通过本政策中“联系我们”一节提供的联系方式与我们联系。</strong>
</p>
<p><strong>一、定义及适用范围</strong></p>
<p><strong>(一)定义</strong></p>
<p><strong>1、羽声语音软件及相关服务指由陕西启星汇申网络科技有限公司陕西省咸阳市高新技术产业开发区高科三路科技企业孵化园3栋4层
及其关联方通过合法拥有并运营的、标注名称为羽声语音、羽声语音极速版的客户端应用程序、APP、微信小程序、微信公众号、羽声语音官方网站 </strong><u>https://golden-llama-1g494r.mysxl.cn/</u>
<strong>、通过APK、SDK、API等方式向您提供的软件和/或服务及随技术发展出现的新形态向您提供的产品与服务。</strong>
</p>
<p><strong>2、关联方</strong><span style="color: rgb(51, 51, 51);">:指在现在、将来控制</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">陕西启星汇申网络科技有限公司</span><span
style="color: rgb(51, 51, 51);">、受</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">陕西启星汇申网络科技有限公司</span><span
style="color: rgb(51, 51, 51);">控制或与</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">陕西启星汇申网络科技有限公司</span><span
style="color: rgb(51, 51, 51);">处于共同控制下的公司、机构。</span></p>
<p><strong>3、未成年人</strong><span style="color: rgb(51, 51, 51);">:指不满十八周岁的自然人。十六周岁以上的未成年人,以自己的劳动收入为主要生活来源的,视为完全民事行为能力人。</span>
</p>
<p><strong>4、儿童</strong><span style="color: rgb(51, 51, 51);">:指不满十四周岁的未成年人。</span>
</p>
<p><strong>(二)适用范围</strong></p>
<p>
<strong>本政策适用于羽声语音的产品或服务。如我们关联公司的产品或服务中使用了羽声语音直播提供的产品或服务,则本政策同样适用于该部分产品或服务。但请您注意,本政策不适用于以下情况:</strong>
</p>
<p><span
style="color: rgb(51, 51, 51);">1、为我们的产品或服务提供广告服务的第三方的信息收集/处理做法;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2、我们的产品或服务可能会包含或链接至第三方提供的信息与/或第三方服务(包括任何第三方应用、网站、产品、服务等),这些信息与/或服务由第三方负责运营,具体规则请参照该第三方的隐私政策或类似声明;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">3、其他非</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音</span><span
style="color: rgb(51, 51, 51);">向您提供的产品与/或服务内容。</span></p>
<p><strong>二、我们如何收集和使用您的个人信息</strong></p>
<p><span style="color: rgb(51, 51, 51);">我们收集和使用的您的个人信息类型包括两种:第一种:我们产品与/或服务的基本业务功能所必需的信息:此类信息为产品与/或服务正常运行的必备信息,您须授权我们收集。如您拒绝提供,您将无法正常使用我们的产品与/或服务;第二种:拓展业务功能可能需要收集的信息:此信息为非基本业务功能所需的信息,您可以选择是否授权我们收集。如您拒绝提供,将导致拓展业务功能无法实现或无法达到我们拟达到的效果,但不影响您对基本业务功能的正常使用。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">我们在此特别提醒您:我们致力于打造多样的产品和服务以满足您的需求。因我们向您提供的产品和服务种类众多,且不同用户选择使用的具体产品/服务范围存在差异,故基本/拓展功能及对应收集使用的个人信息类型、范围等会有所区别,请以具体的产品/服务功能为准。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">我们收集和使用您的个人信息的场景有:</span></p>
<p><strong>(一)注册成为我们的用户</strong></p>
<p><span style="color: rgb(51, 51, 51);">当您在</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">上创建</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">账号时,您需要提供您</span><strong>本人的实名手机号码、验证码</strong><span
style="color: rgb(51, 51, 51);">并完善相关的网络身份识别信息(包括</span><strong>头像、昵称、登录密码、性别、生日</strong><span
style="color: rgb(51, 51, 51);">)。</span><strong>您需理解手机号码、验证码匹配结果、性别、生日均属于您的敏感个人信息我们收集您的手机号码、验证码是基于法律法规的相关要求。其中1实名手机号码、验证码是为了验证您的身份满足相关法律规定的网络实名制要求2生日是为了协助判断您是否为年满十八周岁的成年人若您是未成年人我们将不对您提供服务请您谅解也提请您按真实情况填写相应信息切勿虚构、假冒3性别是为了向您提供性别匹配服务。上述敏感个人信息您有权拒绝提供但将无法使用相关功能或服务请您谨慎考虑后再选择是否提供。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">在您注册账号后,您可以根据自身需求选择</span><strong>填写学校、个性签名、家乡、兴趣爱好等个人主页信息,来帮助您更加形象的展示自我</strong><span
style="color: rgb(51, 51, 51);">。但如果您不提供这些信息,将不会影响本服务的基本业务功能。</span><strong>您在个人主页发布的上述信息、动态、添加的兴趣标签、游戏名片以及你的账号等级、ID号、获赞数、粉丝数将会在账号包括APP和微信小程序的个人主页中公开展</strong><span
style="color: rgb(51, 51, 51);">您也可以通过APP设置-隐私进行信息隐藏(不对未关注用户显示可见)或通过第六节所述管理您的个人信息。</span>
</p>
<p><strong>(二)进行实名认证</strong></p>
<p><span style="color: rgb(51, 51, 51);">1、为满足相关法律规定及监管要求、确保用户身份真实性、实现反欺诈等风控、保障系统与服务安全包括依法保护未成年人合法权益、用户权益发放、打击电信网络诈骗等需要当您使用</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">中某些特定类型的产品与/或服务,</span><strong>您需要向平台提供您本人的真实姓名及身份证号码信息(敏感个人信息),并可能需要通过您的人脸信息(敏感个人信息)进行身份一致性核验</strong><span
style="color: rgb(51, 51, 51);"></span></p>
<p><span style="color: rgb(51, 51, 51);">1在您进行实名认证时</span><strong>若您选择或产品页面进入火山引擎、腾讯云等第三方的实名认证程序(人脸识别功能),我们及/或第三方会申请、调用终端设备的摄像头权限来采集您的面部识别信息在您授权后前述第三方实名认证程序将通过SDK方式获取您的如下敏感个人信息姓名、身份证件号码、人脸信息指为验证或识别您的身份而采集的人脸图像、视频以及基于上述图像、视频提取的面部识别特征为了准确验证您的身份第三方需将上述信息与其合法存有的如基于法律法规或政府部门、有权机关授权或存储您的个人信息进行比对核验。为了验证实名认证结果的有效性并及时解决用户就实名认证结果的争议或投诉您通过以上方式实名认证的第三方会向我们返回人脸识别验证结果以及为验证或识别您的身份而采集的部分人脸图像、视频具体以第三方提供为准此为您的敏感个人信息除此外我们会存储您在上述实名认证过程中主动填写的姓名、身份证号码敏感个人信息</strong>
</p>
<p><strong>当您通过本平台主动选择进行真人头像认证时经您同意后我们会收集您上传的头像照片为了验证您上传的头像照片为您本人不存在冒用或盗用他人照片、头像的情形我们将请求您按上述方式通过第三方人脸识别程序完成实名认证经第三方反馈验证一致的您上传的头像照片将作为您在羽声语音直播APP内的头像进行公开具体规则请见《真人头像认证协议》。因此我们在此提醒您谨慎使用真人头像认证功能您有权拒绝且您拒绝不影响您继续使用本平台的基本业务功能。</strong>
</p>
<p>
<strong>具体使用的第三方人脸识别程序可通过第四节第(一)条所列《羽声语音直播与第三方共享个人信息清单》查阅。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">2若您无法通过上述程序完成实名认证您可以选择通过人工认证方式完成实名认证程序为了完成实名认证服务我们将依据相关法律法规获取并存储您的个人身份信息包括如下</span><strong>姓名(敏感个人信息)、身份证件类型(包括但不限于相应的身份证、护照、台湾居民来往大陆通行证、港澳居民往来内地通行证等)和相应的证件正反面图片(敏感个人信息)、您手持相应身份证件的照片(敏感个人信息)。</strong>
</p>
<p><strong>提请您特别注意,您的身份证件信息、您手持身份证件的照片、真人照片、人脸信息均属于敏感个人信息,您可以拒绝提供,如果拒绝提供您将可能无法获得需要实名认证的特定服务,但不影响您正常使用产品的基本业务功能(包括产品的浏览和观看功能);同时,我们将依法记录、保存验证身份信息及验证结果,这些信息仅供完成验证目的,或者其他法律法规所规定的用途,未经您明示授权不会用作其他目的。</strong>
</p>
<p><strong>2、具体实名场景及对应服务主要包括具体以您使用的功能为准</strong></p>
<p><span
style="color: rgb(51, 51, 51);">1当您申请注册为互联网发布者主播、开设个人房时</span><strong>需要提供您的真实姓名、身份证件信息(敏感个人信息)进行实名身份认证,并通过您的面部识别信息(敏感个人信息)进行身份一致性核验</strong><span
style="color: rgb(51, 51, 51);">。通过验证后,您方可在平台上从事直播发布活动。如果您拒绝提供上述信息(您有权拒绝),您将可能无法获得相关服务(开播或开设个人房间),但不影响您正常使用产品的浏览和观看功能。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2当您通过</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">进行充值、消费时(</span><strong>特别是当您使用下单功能时</strong><span
style="color: rgb(51, 51, 51);">),为了保证您的资金安全,避免未成年人使用下单功能,我</span><strong>们可能需要您提供您的真实姓名、身份证件信息(敏感个人信息)进行实名身份认证,并可能需要您通过您的面部识别信息(敏感个人信息)进行身份一致性核验</strong><span
style="color: rgb(51, 51, 51);">。前述信息您可以拒绝提供,您拒绝提供前述信息的,只会使您无法使用</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">对应功能,但不影响您使用其他功能。</span></p>
<p><span style="color: rgb(51, 51, 51);">3当您使用提现功能</span><strong>们会需要您绑定银行卡账号信息、支付宝账号、身份证号码、姓名(敏感个人信息)进行实名认证。在您授权后,我们可能需要通过您的面部识别信息(敏感个人信息)进行身份一致性核验</strong><span
style="color: rgb(51, 51, 51);">。您可以拒绝提供前述信息,但您拒绝提供将导致您无法使用对应功能,但不影响您使用</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">的其他功能。</span></p>
<p><span
style="color: rgb(51, 51, 51);">4在账号冻结、封禁申诉时为了核实争议账号是您本人持有平台还将使用您注册时使用的</span><strong>实名手机号码、身份证件信息(敏感个人信息)进行身份核实认证</strong><span
style="color: rgb(51, 51, 51);"></span></p>
<p><span
style="color: rgb(51, 51, 51);">5基于依法保护未成年人权益的需求我们为未成年人提供了青少年模式若您拟解除、退出青少年模式我们会需要您提</span><strong>供真实姓名、身份证件信息(敏感个人信息),并在您授权后,我们可能需要通过您的面部识别信息进行身份一致性核验</strong><span
style="color: rgb(51, 51, 51);">。如果您拒绝提供前述信息,您将无法退出青少年模式,但不影响您在青少年模式下使用</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">产品和服务。</span></p>
<p><strong>(三)为您提供服务内容的浏览/播放/搜索/下载</strong></p>
<p><span style="color: rgb(51, 51, 51);">1、</span><strong>当您进行直播间、聊天室、视频、音频等服务内容的浏览、播放、下载功能时,为与您的移动设备进行必要的适配,向您提供连续性、一致化的使用体验以及最优化的页面展示,我们需要收集您的设备信息、个人上网记录。</strong>
</p>
<p><strong>设备信息包括浏览器类型、设备型号、操作系统版本、设备设置、DEVICEID、AndroidID、MAC地址、IMEI、IDFA、OAID、ICCID、UDID、UUID、MEID及其他设备标识符、设备环境。</strong>
</p>
<p><strong>个人上网记录包括:浏览/点击/播放记录、收藏/关注/预约记录、点赞/分享/发布记录等。</strong></p>
<p><span style="color: rgb(51, 51, 51);">我们收集这些信息是为了向您提供直播间、视频、音频等内容的浏览、播放和展示服务,如您拒绝提供上述权限将可能导致您无法使用我们的相关产品与服务。请您理解,单独的设备信息、日志信息是无法识别特定自然人身份的信息。如果我们将这类非个人信息与其他信息结合用于识别特定自然人身份,或者将其与个人信息结合使用,则在结合使用期间,这类非个人信息将被视为个人信息,除取得您授权或法律法规另有规定外,我们会将该类个人信息做匿名化、去标识化处理。</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">2、当您使用我们提供的搜索功能时我们还会收集您搜索的关</span><strong>键字信息、设备信息包括设备型号、操作系统版本、设备设置、AndroidID、MAC地址、IMEI、IDFA、OAID、ICCID等设备标识符、设备环境、日志信息包括您的浏览记录和时间、您搜索的时间以及次数</strong><span
style="color: rgb(51, 51, 51);">。我们收集这些信息是为了方便您后续查找时无需再次输入,并向您快速匹配您所需要的内容以及您可能感兴趣的内容,为了提供高效的搜索服务,部分前述信息会暂时存储在您的本地存储设备之中,并可向您展示搜索结果内容、搜索历史记录。您可以自主删除前述搜索记录。搜索的关键词信息通常无法单独识别您的个人身份,不属于您的个人信息,不在本政策的限制范围内。但当您的搜索关键词信息与您的其他信息有联结并可识别您的个人身份时,在结合使用期间,我们会将您的搜索关键词信息作为您的个人信息,与您的搜索历史记录一同按照本政策对其进行处理与保护。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">当您分享/接收被分享的信息时,我们会在您进入</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">时访问您的剪切板,写入或读取其中包含的文字、链接、口令、分享码,以实现分享、跳转功能。我们不会收集、存储剪切板的其他信息。</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">3、当您使用下载功能时经您同意后我们会访问您的</span><strong>存储权限</strong><span
style="color: rgb(51, 51, 51);">,以便在您设备外置存储空间内写入相关文件。如您拒绝提供,仅会使您无法使用该功能,但并不影响您正常使用产品或服务的其他功能。同时,您也可以随时通过您设备的相关功能设置开启/取消该权限。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">(五)</span><strong>帮助您上传或发布图文、音频和视频</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">1、若您通过</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">的社区功能或通过</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">的评论、分享、发帖、发布动态等功能上传/发布/编辑图文、音频、视频时,当您使用到具体功能时,我们将向您分别单独请求</span><strong>访问您设备的相机/摄像头权限、相册权限IOS端/存储权限(安卓端)、录音/麦克风权限1经您同意授权相机/摄像头权限后,我们会访问您的相应权限,以便您拍摄照片、视频,并采集、识别上述功能实现过程中所拍摄或录制的图片及视频信息</strong><span
style="color: rgb(51, 51, 51);"></span><strong>2经您同意授权相册权限IOS端/存储权限(安卓端)后,我们会访问您的相应权限,以便您查看、选择相册里的图片/视频发布动态、反馈意见,设置与更换头像、封面,聊天时分享图片/视频,以及保存图片/视频至相册您还可以在其他场景访问设备里的照片、音频和视频以及保存内容到设备3经您同意授权录音/麦克风权限后,我们会访问您的相应权限,以便识别您上传的语音信息,并采集、识别上述功能实现过程中所录制、输入的音频信息。如您拒绝提供上述权限的仅会使您无法使用该功能,但并不影响您正常使用产品/服务的其他功能,同时,您也可以随时通过您的设备系统或使用我们的产品/服务的相关功能设置页面开启/取消该权限。</strong>
</p>
<p><span
style="color: rgb(51, 51, 51);">2、如您自愿填写信息或自愿发布内容的我们会收集您上传的</span><strong>视频、照片、音视频、帖子、评论、点赞、关注、分享信息、浏览及搜索信</strong><span
style="color: rgb(51, 51, 51);">息。您可随时删除您发布的照片、音视频、帖子及评论内容。为了验证您发布内容的合法合规性,避免您发布的内容涉及违法或侵犯第三方合法权益的情形。您</span><strong>同意我们为了上述目的将您发布的图文、音频、视频内容提供给我们的服务提供商,并由我们的服务提供商对上述内容进行审核、标注后向我们反馈结果。</strong><span
style="color: rgb(51, 51, 51);">我们不会向上述服务商传输或共享您的个人信息,我们仅会对您发布内容的合法合规性进行审核。</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">3、当您使用设备相机扫描二维码时我们将访问您设备的</span><strong>相机/摄像头权限</strong><span
style="color: rgb(51, 51, 51);">。如您拒绝提供的仅会使您无法使用该功能,但并不影响您正常使用产品与/或服务的其他功能。同时,您也可以随时通过您的设备的设置开启/取消该权限。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">4、您在使用动态发布等功能时发布的公开信息可能涉及您或他人的个人信息或个人隐私信息如您在分享、评论时选择上传的包括个人信息的图片或视频。</span><strong>您应更为谨慎考虑是否公开该等信息,以及获得合法权利人授权后再行发布,同时如该等信息中涉及未成年人个人信息的,您应在发布前征得其监护人同意。</strong>
</p>
<p><strong>(六)录制音视频及提供直播功能</strong></p>
<p><span
style="color: rgb(51, 51, 51);">当您使用录制音视频、使用个人房间服务以及直播服务时,经您分别授权我们将</span><strong>访问您设备的相机/摄像头权限(当您同意开启此项权限后,我们会使用您设备上的相机拍摄功能。此项功能将用于实现照片拍摄、录制视频功能,并采集、识别上述功能实现过程中所拍摄或录制的图片及视频信息)、录音(麦克风)相关权限(当您同意开启此项权限后,我们会使用您设备上的麦克风功能,并会识别您上传的语音信息。此项权限将用于实现语音输入功能,并采集、识别上述功能实现过程中所输入的语音信息)</strong><span
style="color: rgb(51, 51, 51);">,如您拒绝授权的仅会使您无法使用该功能,但并不影响您正常使用产品与/或服务的其他功能。同时,您也可以随时通过您的设备设置开启/取消该权限。此外,</span><strong>为保证您使用直播或聊天室功能时的流畅度、节省您的流量,我们会收集您使用该功能时段内使用流量的大小。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">(七)消费与提现</span></p>
<p><span style="color: rgb(51, 51, 51);">当您通过</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音</span><span
style="color: rgb(51, 51, 51);">购买商品和/或服务时如SVIP会员购买、购买虚拟商品、个性装扮、礼物为帮助您完成交易、保障交易安全、提供订单查询、客服及售后服务</span><strong>我们需要根据商品的类型收集如下部分或全部的订单信息:下单时间、订单编号、订单状态、交易的商品信息、支付信息(支付金额、支付方式、支付流水号、支付状态、第三方支付渠道的支付账号,敏感个人信息)、虚拟商品消费使用记录、会员开通时间</strong><span
style="color: rgb(51, 51, 51);">。为此为保障您的账户和资金安全以及支付服务的安全稳定运行、履行反电信网络诈骗等法定义务我们还会收集您必要的设备信息IP地址并提供给您选择的第三方支付渠道。为向您提供售后与争议解决服务之目的我们需要及时获悉并确认交付、售后的进度及状态您同意我们可自相应第三方处收集与交付、售后进度相关的信息。我们向您承诺我们会以最大努力保障您的个人信息安全并严格要求第三方对您的个人信息保密只以交付、售后之目的获悉和使用不得对外泄露或做其他任何用途。您可以拒绝提供前述信息但您拒绝提供将导致您无法使用对应功能但不影响您使用</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">的基本业务功能。</span></p>
<p><span style="color: rgb(51, 51, 51);">当您使用提现功能时,</span><strong>为了完成支付结算,我们会收集、使用您主动填写的收款账户信息(银行卡账号信息及开户行信息、支付宝账号及/或微信账号,具体以您填写为准)以及身份证号码、姓名,并会共享给第三方支付结算机构(请见第四节第(一)项)。前述信息为敏感个人信息,您可以拒绝提供,您拒绝提供将导致您无法使用对应提现功能,但不影响您使用羽声语音直播的其他功能。</strong>
</p>
<p><strong>(八)客户服务</strong></p>
<p><strong>当您主动填写羽声语音APP上的反馈意见如帮助与反馈-意见反馈页面)、举报投诉页面时,我们会收集您主动填写的手机号、反馈/举报投诉内容(请谨慎考虑后再决定是否填写),我们收集信息仅用于收集您的意见,便于与您联系、尽快帮助您解决问题。</strong>
</p>
<p><span
style="color: rgb(51, 51, 51);">当您向我们发起投诉、申诉或进行咨询时,为了保障您的账户及系统安全,</span><strong>我们需要您提供必要的个人信息(包括账号信息、姓名、手机号、身份证以及其他必要身份信息)以核验您的用户身份</strong><span
style="color: rgb(51, 51, 51);">。为</span><strong>便于与您联系、尽快帮助您解决问题或记录相关问题的处理方案及结果,我们可能会收集您与我们的沟通信息(包括文字/图片/音视频/通话记录形式)、您为了证明相关事实提供的信息、与客服需求相关联的其他必要信息。如您针对具体的订单发起客服需求的,我们还会使用您的订单信息。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">以上信息为您的</span><strong>个人敏感信息</strong><span
style="color: rgb(51, 51, 51);">,您有权拒绝提供,但我们收集这些信息是为了调查事实与帮助您解决问题,如您拒绝提供上述信息,我们可能无法向您及时反馈投诉、申诉或咨询结果。</span>
</p>
<p><strong>(九)为您提供安全保障服务</strong></p>
<p><span style="color: rgb(51, 51, 51);">为了使</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">产品及服务与您的移动设备进行必要的适配,提高您使用我们及我们合作伙伴提供服务的安全性,保护您或其他用户或公众的人身财产安全免受侵害,更好的预防钓鱼网站、欺诈、网络漏洞、计算机病毒、网络攻击、网络入侵等安全风险,分析当前设备的风险等级,有效识别各种作弊行为、风险设备、恶意刷量、恶意程序、保护您的账号安全,我们会收集为实现安全保障功能的必要信息,我们会获取您的以下信息:</span>
</p>
<p><strong>1、网络访问日志信息和用户日志信息。包括网络信息如网络类型、运营商信息如SIM卡信息、账号信息如账号登录地以及使用“羽声语音直播”产品或服务的频率、崩溃数据、总体安装、使用情况、性能数据、服务故障等信息。</strong>
</p>
<p><strong>2、您的设备信息。包括设备型号、操作系统、设备序列号/SN、设备标识符如IMEI/IMSI/Android
ID/IDFA/OpenUDID/GUID/ICCID/
UUID/MEID/DEVICEID、设备MAC地址、应用列表、登陆IP地址、软件版本号、接入网络的方式、类型和状态、设备当前运行进程/运行中进程信息、设备传感器信息、蓝牙信息屏幕尺寸等参数信息同时为了保证您的账号安全我们会检测您设备的root状态</strong><span
style="color: rgb(51, 51, 51);">。请您注意为了便于您及时接收、发送聊天消息、接收消息通知我们会在后台状态下获取您设备的MAC地址。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">我们可能会使用或整合您的上述日志信息与设备信息、账户信息、交易信息、支付信息以及其他取得您授权或依据法律共享的信息,综合判断您账户及交易风险、完成身份验证、检测,防范安全事件,并依法采取必要的记录、审计、分析、处置措施,保护各方合法权益稳定不受侵害。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">您理解并同意为了帮助我们进行bug分析、反作弊、反黑产等安全保护工作保障您正常使用本产品与/或服务、保障您的账号安全,</span><strong>在您的羽声语音App每次切换至后台或处于静默状态下或重新启动时我们会收集您的Mac地址信息包括手机Mac地址和蓝牙Mac地址、IMEI、获取运行中进程信息、设备序列号、Android
ID、DEVICEID我们将把读取频次控制在合理范围内。</strong></p>
<p><strong>请您理解,单独的设备信息、日志信息等是无法识别特定自然人身份的信息。如果我们将这类非个人信息与其他信息结合用于识别特定自然人身份,或者将其与个人信息结合使用,则在结合使用期间,这类非个人信息将被视为个人信息,除取得您授权或法律法规另有规定外,我们会将该类个人信息做匿名化、去标识化处理。</strong>
</p>
<p><strong>同时为了维护网络安全、保障良好生态便于公众为公共利益实施监督依据《互联网用户账号信息管理规定》等法律规定您的IP地址所对应的归属地域信息将会展示在您的个人资料页。境内展示到省区、市境外展示到国家或地区信息以网络运营商数据为准请您理解该信息不会泄露您的详细地址相关展示暂不支持手动开启或关闭。</strong>
</p>
<p><strong>(十)我们获取的设备权限</strong></p>
<p><span style="color: rgb(51, 51, 51);">为确保相关业务功能的正常实现,我们需要根据具体的使用场景调用对应的必要权限。请您注意,您开启任一权限即代表您授权我们可以收集和使用相关个人信息来为您提供对应服务,若您关闭任一权限或取消授权,我们将不再基于对应权限继续收集和使用相关个人信息,也无法为您提供该权限所对应的服务。但是,您关闭权限的决定不会影响此前基于您的授权所进行的信息收集及使用。</span>
</p>
<p><strong>(十一)收集和使用个人信息的其他规则</strong></p>
<p><span
style="color: rgb(51, 51, 51);">1、若您提供的信息中含有其他用户的个人信息在向我们提供这些个人信息之前您需确保您已经取得合法的授权。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2、您理解并知悉您向外部第三方</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">关联公司不在此限)</span><strong>提供的个人信息或外部第三方收集的您的个人信息我们无法获取更不会使用非常规方式恶意干预对方系列APP数据擅自以软件程序获得您的个人信息。羽声语音直播可能因业务发展的需要而确实需要从第三方间接收集如共享等您的个人信息的且由我们直接或共同为您提供产品或服务的我们会在收集前明确以书面形式要求该第三方说明其个人信息来源以及是否已经就其收集、处理以及向我们提供您的个人信息取得了您的合法授权如果使用方式和范围超出您在第三方原授权范围的我们会再次征得您的授权同意。我们的某些产品或服务由第三方业务合作伙伴提供或共同提供时为了必要且合理的开展业务我们可能会从部分业务合作伙伴处间接收集的您的部分信息、其他方使用我们的产品/服务时所提供有关您的信息。如果第三方的授权范围无法涵盖我们的处理和使用目的时,我们会自行或者要求该第三方征得您的同意后再行处理您的个人信息。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">3、如果某一需要收集您的个人信息的产品/服务未能在本隐私政策中予以说明的,或者我们超出了与收集您的个人信息时所声称的目的及具有直接或合理关联范围的,我</span><strong>们将在收集和使用您的个人信息前,通过更新本隐私政策、页面提示、弹窗、站内信、网站公告或其他便于您获知的方式另行向您说明,并为您提供自主选择同意的方式,且在征得您明示同意后收集和使用。</strong>
</p>
<p><strong>4、征得授权同意的例外</strong></p>
<p><strong>您理解并同意,在以下情况下,我们无需取得您的授权同意即可收集和使用您的个人信息:</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">1与国家安全、国防安全有关的</span></p>
<p><span style="color: rgb(51, 51, 51);">2与公共安全、公共卫生、重大公共利益有关的</span></p>
<p><span style="color: rgb(51, 51, 51);">3与犯罪侦查、起诉、审判和判决执行等有关的</span></p>
<p><span
style="color: rgb(51, 51, 51);">4出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到您本人同意的</span>
</p>
<p><span style="color: rgb(51, 51, 51);">5所收集的个人信息是您自行向社会公众公开的</span></p>
<p><span style="color: rgb(51, 51, 51);">6从合法公开披露的信息中收集个人信息的如合法的新闻报道、政府信息公开等渠道并依照《个人信息保护法》规定在合理的范围内处理您自行公开或者其他已经合法公开的个人信息</span>
</p>
<p><span style="color: rgb(51, 51, 51);">7根据您的要求签订合同所必需的</span></p>
<p><span
style="color: rgb(51, 51, 51);">8用于维护所提供的产品或服务的安全稳定运行所必需的例如发现、处置产品或服务的故障</span>
</p>
<p><span style="color: rgb(51, 51, 51);">9为合法的新闻报道所必需的</span></p>
<p><span style="color: rgb(51, 51, 51);">10学术研究机构基于公共利益开展统计或学术研究所必要且对外提供学术研究或描述的结果时对结果中所包含的个人信息进行去标识化处理的</span>
</p>
<p><span style="color: rgb(51, 51, 51);">11法律法规规定的其他情形。</span></p>
<p><span style="color: rgb(51, 51, 51);">我们需要特别提醒您的是:</span><strong>由于我们的产品和服务较多,为您提供的内容也不同,因此收集的您的个人信息也会因产品/服务的内容不同而有所区别,具体以产品/服务实际提供为准。我们可能会不时推出新的或优化后的功能,或者依据最新法律法规要求,可能需要收集、使用您的新的个人信息或变更个人信息使用目的或方式。届时,我们将通过更新本政策、弹窗、页面提示等方式另行向您说明并为您提供自主选择同意的方式,且在征得您同意后再收集、使用。</strong>
</p>
<p><strong>三、我们如何使用Cookie和第三方H5页面</strong></p>
<p><span style="color: rgb(51, 51, 51);">为使您获得更轻松的访问体验,您使用</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">产品或服务时,我们可能会通过采用各种技术收集和存储您访问</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">服务的相关数据,在您访问或再次访问</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">服务时,我们能识别您的身份,并通过分析数据为您提供更好更多的服务。包括使用小型数据文件识别您的身份,这么做是为了解您的使用习惯,帮您省去重复输入账户信息的步骤,或者帮助判断您的账户安全。这</span><strong>些数据文件可能是Cookie、Flash
Cookie或您的浏览器或关联应用程序提供的其他本地存储统称“Cookie”</strong></p>
<p><span style="color: rgb(51, 51, 51);">网页上常会包含一些电子图像称为“单像素GIF文件”或“网络beacon”它可以帮助网站计算浏览网页的用户或访问某些cookie。我们会通过网络beacon收集您浏览网页活动信息例如您访问的页面地址、您先前访问的援引页面的位址、您停留在页面的时间、您的浏览环境以及显示设定等。</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">如果您的浏览器或浏览器附加服务允许您可以修改对Cookie的接受程度或者拒绝</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">的Cookie在浏览器具备该功能的前提下可以通过您的浏览器的设置以管理、部分或全部拒绝Cookie与/或同类技术或删除已经储存在您的计算机、移动设备或其他装置内的Cookie与/或同类技术,从而实现我们无法全部或部分追踪您的个人信息。您如需详细了解如何更改浏览器设置,请具体查看您使用的浏览器的相关设置页面),但拒绝</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">的Cookie可能无法使用由我们提供的、依赖于Cookie的功能或服务如有</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">第三方H5页面。为了提升服务便捷性、方便用户快速触达第三方服务我们可能会接入</span><strong>第三方的H5服务页面入口如果您进入这些H5页面该第三方可能会收集你的手机号、身份证件号、付款账号信息请注意您在以上第三方H5页面进行的各项操作请遵守该等第三方的隐私保护政策。当您离开我们平台跳转至第三方H5页面时我们提醒您注意保护个人隐私。本平台不收集、不存储您在该类第三方服务页面填写和提交的任何个人信息。</strong>
</p>
<p><strong>四、我们如何共享、转让、公开披露您的个人信息</strong></p>
<p><strong>(一)共享</strong></p>
<p>
<strong>我们不会与陕西启星汇申网络科技有限公司及关联方以外的任何公司、组织和个人分享您的个人信息,但以下情况除外:</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">1、获得您及监护人明确同意或授权的共享</span></p>
<p><span style="color: rgb(51, 51, 51);">2、基于法定情形提供。根据法律法规的规定、诉讼争议解决需要、您与我们签署的相关协议(包括在线签署的电子协议及平台规则)或法律文件,或行政、司法等有权机关依法提出要求时,我们可能会共享您的个人信息;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">3、提供统一管理服务。为</span><strong>便于您统一管理账号,体验更优质的服务,我们可能会向我们的关联公司共享您的个人信息。但我们只会共享必要的个人信息,如果我们共享您的个人敏感信息或者关联方改变个人信息的使用目的,将再次征求您的授权同意;</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">4、提供必要的合作服务并保证服务安全、优化。</span><strong>我们提供的产品/服务大多是无法单独完成的故我们的某些服务将由业务合作伙伴提供。为保障为您提供的服务顺利完成我们可能会将您的个人信息共享给我们的合作伙伴包括基础技术服务、支付服务、营销活动等的合作商、第三方商家、广告商等同时为了向您提供更完善、优质、安全的产品和服务我们的应用中会嵌入授权合作伙伴的软件工具开发包本政策中简称“SDK”或其他类似的应用程序我们的某些服务将由授权合作伙伴提供或使用第三方SDK相关技术为您提供服务。基于此我们会与合作伙伴共享您的某些个人信息或者第三方SDK会通过SDK方式收集您的个人信息</strong><span
style="color: rgb(51, 51, 51);">,具体包括:</span></p>
<p><span style="color: rgb(51, 51, 51);">1</span><strong>第三方账号登录、内容分享</strong><span
style="color: rgb(51, 51, 51);">。QQ、微信、新浪微博会通过SDK方式收集您的</span><strong>位置信息、网络信息包括Wi-Fi信息、设备标识信息(MAC地址、IMEI、IMSI)、运营商信息、外部存储设备信息,用于实现第三方账号授权登录、共享发布的内容及评论等信息。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">2</span><strong>安全监测与风险防范</strong><span
style="color: rgb(51, 51, 51);">。为了保障您的账号安全避免您遭受欺诈、网络病毒、网络攻击等风险为我们提供安全保障的合作伙伴会通过SDK方式收集与您有关的必要</span><strong>设备信息、日志信息,以及您的使用习惯和常用软件信息等来综合判断您的账号及交易风险,预防安全事件的发生。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">3</span><strong>支付与结算功能。</strong><span
style="color: rgb(51, 51, 51);">支付功能由与我们合作的第三方支付机构向您提供服务基于此支付宝、微信、银联会通过SDK方式收集您的</span><strong>手机号、设备信息识别码(包括IMSI)、位置信息、网络信息包括Wi-Fi信息、当前运行进程/获取运行中进程信息、屏幕参数、运营商信息</strong><span
style="color: rgb(51, 51, 51);">,保证充值、消费、支付功能的正常使用,这些信息是支付功能所必需的信息,拒绝提供将导致您无法使用该功能。</span><strong>当您提现收入(如有)时,为了您能及时完成收入的结算与提现、依据法律规定完成税费缴纳,将由与您另行单独确认的第三方服务方向您提供结算、税费扣缴服务,基于此,我们会向该类第三方服务方共享您提现时主动提供的收款账户信息以及身份信息,具体信息详见第二节第(七)款。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">4人脸识别。阿里云、腾讯云、字节跳动会通过SDK方式收集您的证件信息、设备标识信息、网络信息包括Wi-Fi信息、设备传感器信息、屏幕参数、应用列表该等第三方获取的面部特征信息用于人脸核验我们只获得核验结果不保存您的面部特征信息。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">5直播视频服务。声网、会</span><strong>通过SDK收集您的共享Wi-Fi信息、设备标识信息、地理位置信息、运营商信息、外部存储设备、当前运行进程/获取运行中进程信息、屏幕参数,并共享用户授予的读取/写入外置存储、位置权限、获取设备信息、相机/摄像头权限、读取手机/电话状态用于聊天室内和IM音视频通话图片上传、直播推流和直播回放功能等相关功能。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">目前,我们接入的第三方服务商主要包括以下几种类型:</span></p>
<p><span style="color: rgb(51, 51, 51);">用于消息推送功能包括手机厂商Push推送、特定事件提醒</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">用于支付相关服务,包括订单支付、交易行为核验、收入结算、支付信息汇总统计;</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">用于在您同意的情况下获取设备位置权限、搜集设备信息和日志信息;</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">用于第三方授权服务,包括第三方账号登陆、将有关内容分享至第三方产品;</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">用于优化产品性能,包括提高硬件配网能力、降低服务器成本、功能热修复;</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">用于账号安全、产品加固相关服务,包括网络监测、域名解析、防劫持、防黑产、反作弊、加解密服务。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">我们接入的部分第三方SDK可能会收集您的个人信息如您在我们的服务中使用这类由第三方提供的服务时您同意将由其直接收集和处理您的信息。我们会评估这类第三方服务收集个人信息的合法性、正当性、必要性要求该等第三方对您的个人信息采取保护措施并严格遵守相关法律法规与监管要求。您可以查看下方《语音接入第三方SDK目录》了解我们主要接入的第三方SDK基本情况。</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">羽声语音直播接入第三方 SDK 目录(与全国平台登记信息完全一致)</span>
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">1、</span><span
style="color: rgb(51, 51, 51);">SDK 名称APP 支付客户端 SDK<br>开发者信息:支付宝(杭州)信息技术有限公司<br>服务类型:唤起支付宝 App 完成支付<br>所涉信息:设备 MAC 地址、网络访问、WiFi 状态、读取电话状态、写入外部存储<br>隐私政策链接:</span>https://opendocs.alipay.com/open/54/01g6qm
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">2、</span><span
style="color: rgb(51, 51, 51);">SDK 名称QQ 互联 SDKQQ 开放平台)<br>开发者信息:深圳市腾讯计算机系统有限公司<br>服务类型QQ 分享、授权登录<br>所涉信息设备标识信息IMEI/AndroidID/ICCID、定位信息、地理位置、ANDROID_ID<br>隐私政策链接:</span>https://connect.qq.com/index.html
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">3、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:微信 OpenSDK Android<br>开发者信息:深圳市腾讯计算机系统有限公司<br>服务类型:微信分享、微信登录、微信支付、微信客服<br>所涉信息设备标识信息IMEI/AndroidID/ICCID、定位信息、地理位置、ANDROID_ID<br>隐私政策链接:</span>https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/RYiYJkLOrQwu0nb8
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">4、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:声网音视频 SDK含音频 RTC<br>开发者信息:上海声网科技有限公司<br>服务类型:提供实时音频服务<br>所涉信息:设备 MAC 地址、IP 地址、网络访问、WiFi 状态、设备信息、文件存储权限、麦克风权限<br>隐私政策链接:</span>https://www.shengwang.cn/SDK-privacy-policy/
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">5、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:腾讯云即时通信 IM SDK<br>开发者信息:腾讯云计算(北京)有限责任公司<br>服务类型:即时通信(私聊、群聊、房间内公屏聊天)<br>所涉信息:设备 MAC 地址、IP 地址、网络状态、文件存储权限、麦克风权限、相机权限<br>隐私政策链接:</span>https://cloud.tencent.com/document/product/269/58094
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">6、</span><span
style="color: rgb(51, 51, 51);">SDK 名称ShareSDK含友盟统计 / 友盟推送)<br>开发者信息广州掌淘网络科技有限公司ShareSDK<br>服务类型:社会化分享、统计分析、消息推送<br>所涉信息设备标识符IMEI/Mac/android ID/IDFA/OPENUDID/GUID、SIM 卡 IMSI 信息、社交账号公开信息、地理位置、IP 地址、应用列表<br>隐私政策链接:</span>https://www.umeng.com/page/policy
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">7、</span><span
style="color: rgb(51, 51, 51);">SDK 名称Bugly Android<br>开发者信息:深圳市腾讯计算机系统有限公司<br>服务类型:定位崩溃、异常上报与运营统计<br>所涉信息日志信息自定义日志、Logcat 日志、崩溃堆栈)、设备 IDandroidid / idfv、联网信息、系统名称、系统版本、国家码<br>隐私政策链接:</span>https://privacy.qq.com/document/preview/fc748b3d96224fdb825ea79e132c1a56
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">8、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:腾讯云慧眼人脸核身增强版 SDK<br>开发者信息:腾讯云计算(北京)有限责任公司<br>服务类型:实名认证<br>所涉信息个人身份信息真实姓名、身份证号、面部识别特征、读取短信、联系人、通话记录、位置信息、设备标识信息IMEI、IMSI、AndroidID<br>隐私政策链接:</span>https://cloud.tencent.com/document/product/1007/66043
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">9、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:音视频通话 TRTC SDKAndroid<br>开发者信息:深圳市腾讯计算机系统有限公司<br>服务类型:即时语音通话(私聊、群聊、房间内公屏聊天)<br>所涉信息:设备 MAC 地址、IP 地址、网络状态、文件存储权限、麦克风权限、相机权限<br>隐私政策链接:</span>https://cloud.tencent.com/document/product/647/57574
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">10、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:号码认证-android阿里云一键登录<br>开发者信息:阿里巴巴云计算(北京)有限公司<br>服务类型:手机号一键登录<br>所涉信息网络类型、设备信息IP 地址、设备制造商、设备型号、手机操作系统、SIM 卡状态)、运营商类型、本机号码<br>隐私政策链接:</span>https://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud202112211045_86198.html
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">11、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:对象存储 OSS Android SDK阿里云<br>开发者信息:阿里云计算有限公司<br>服务类型:平台图片、视频、礼物等文件存储<br>所涉信息:设备型号、设备操作系统信息(用于兼容性适配与故障排查)<br>隐私政策链接:</span>https://terms.alicdn.com/legal-agreement/terms/privacy_policy_full/20240202100310511/20240202100310511.html
</p>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><span style="color: rgb(51, 51, 51); font-family: 宋体;">12、</span><span
style="color: rgb(51, 51, 51);">SDK 名称:腾讯云 COS 文件存储器 SDK<br>开发者信息:腾讯云计算(北京)有限责任公司<br>服务类型:平台图片、视频、礼物等文件存储<br>所涉信息设备型号、设备操作系统、IP 地址、网络连接类型(用于兼容性与传输优化)<br>隐私政策链接:</span>https://cloud.tencent.com/document/product/454/61839
</p>
<p><br></p>
<p>13、<span
style="color: rgba(0, 0, 0, 0.9); background-color: rgb(255, 255, 255); font-size: 16px;">SDK 名称:联通认证 SDK<br>开发者信息:联通在线信息科技有限公司<br>服务类型:手机号一键登录 / 本机号码校验<br>所涉信息网络类型、网络地址IP 地址)、运营商类型、本机手机号、手机设备类型、手机操作系统、硬件厂商<br>隐私政策链接:</span><a
href="https://msv6.wosms.cn/html/oauth/protocol2.html" target="_blank"
style="text-align: start;">https://msv6.wosms.cn/html/oauth/protocol2.html</a><br></p>
<h3 style="text-align: start;">手机 GPS 定位说明(无第三方 SDK</h3>
<p>功能场景</p>
<ol>
<li style="text-align: start;">
业务功能:判断主播/观众实时地理位置,用于「附近频道」「同城房间」「地区排行榜」「内容合规(区域管制)」等。
</li>
<li style="text-align: start;">
触发时机:仅当用户首次进入需定位的页面时弹窗申请,拒绝不影响基础功能。
</li>
<li style="text-align: start;">所涉信息<br>精确位置GPS 经纬度)、粗略位置(网络定位辅助)、手机方向传感器(用于地图罗盘旋转)。
</li>
<li style="text-align: start;">使用方式</li>
</ol>
<ul>
<li style="text-align: start;">仅 App 前端调用
Android「LocationManager」/iOS「CLLocationManager」系统接口未集成任何第三方定位 SDK。
</li>
<li style="text-align: start;">
定位结果仅在客户端内存临时使用,退出相关页面立即释放,不做后台持续采集、不上传服务器、不存储本地、不与任何第三方共享。
</li>
<li style="text-align: start;">权限管理<br>用户可随时在系统设置-应用权限-位置信息中关闭授权,关闭后涉及位置的功能将提示「需要位置权限」并引导重新开启。
</li>
<li style="text-align: start;">未成年人<br>如用户未满 14 周岁,我们将在征得监护人同意后才会申请位置权限,否则默认关闭定位相关功能。
</li>
</ul>
<p><span style="color: rgb(51, 51, 51);"> </span></p>
<p><strong>自启动和关联启动说明:</strong></p>
<p><strong>为确保本应用处于关闭或后台运行状态下可正常接收到客户端推送的广播信息,本应用须使用(自启动)能力,将存在一定频率通过系统发送广播唤醒本应用自启动或关联启动行为,是因实现功能及服务所必要的。</strong>
</p>
<p><strong>请您知悉1我们仅会出于合法、正当、必要、明确的目的共享您的个人信息并且授权合作伙伴只能接触到其履行职责所需信息。同时我们会与合作伙伴签署严格的保密协定要求他们按照我们的说明、本政策以及其他任何相关的保密和安全措施来处理您的个人信息2前述第三方合作伙伴为数据控制者以其自己的名义获得您的同意以处理您的个人信息。合作方可能有其独立的隐私政策我们建议您认真阅读并遵守第三方的隐私政策。如果您拒绝我们的合作方在提供服务时收集为提供服务所必需的个人信息将可能导致您无法使用相应服务。</strong>
</p>
<p><strong>(二)转让</strong></p>
<p><span
style="color: rgb(51, 51, 51);">我们不会将您的个人隐私信息转让给任何公司组织和个人,但以下情况除外:</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">1、在获取明确同意的情况下共享获得您的明确同意后我们将与其分享您的个人隐私信息</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2、在涉及合并、收购、资产转让、破产清算时你的个人信息有可能因此而被转移如涉及个人隐私信息转让我们将要求受让方继续受此政策约束并告知您受让方身份、联系方式否则我们将要求受让方向您重新索取授权同意。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">(三)公开披露</span></p>
<p><span
style="color: rgb(51, 51, 51);">1、公开披露是指向社会或不特定人群发布信息的行为。我们仅在下列情形下公开披露您的个人隐私信息</span>
</p>
<p><span style="color: rgb(51, 51, 51);">1征得您明确的授权同意</span></p>
<p><span style="color: rgb(51, 51, 51);">2基于国家法律法规的规定而对外披露</span></p>
<p><span
style="color: rgb(51, 51, 51);">3应国家司法机关及其他有关机关基于法定程序的要求而披露</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">4为公共利益实施新闻报道、舆论监督等行为在合理的范围内公开的</span>
</p>
<p><span style="color: rgb(51, 51, 51);">5为应对突发公共卫生事件或在紧急情况下为保护其他用户及第三方生命健康和财产安全而必须披露的在该情况下我们将在紧急情况消除后及时告知您</span>
</p>
<p><span style="color: rgb(51, 51, 51);">6应用户监护人合法要求而提供用户个人身份信息时</span></p>
<p><span
style="color: rgb(51, 51, 51);">7对违规账号、欺诈行为等进行处罚公告、公布中奖/获胜者等名单时脱敏展示相关信息等必要事宜而进行的必要披露。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2、对于公开披露的您的个人信息我们会在收到公开披露申请后第一时间且审慎审查其正当性、合理性、合法性并在公开披露时和公开披露后采取最严格个人信息安全保护措施和手段对其进行保护。</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">(四)共享、转让、公开披露个人信息时事先征得授权同意的例外</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">下述情形中,共享、转让、公开披露您的个人信息无需事先征得您的授权同意:</span>
</p>
<p><span style="color: rgb(51, 51, 51);">1、与国家安全、国防安全直接相关的</span></p>
<p><span style="color: rgb(51, 51, 51);">2、与公共安全、公共卫生、重大公共利益直接相关的</span></p>
<p><span
style="color: rgb(51, 51, 51);">3、与犯罪侦查、起诉、审判和判决执行等直接相关的或根据法律法规的要求、行政机关或公检法等有权机关的要求的</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">4、出于维护您或其他个人的生命、财产等重大合法权益但又很难得到您本人同意的;</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">5、个人信息是您自行向社会公开的或者是从合法公开的渠道如合法的新闻报道、政府信息公开等渠道中收集到的</span>
</p>
<p><span style="color: rgb(51, 51, 51);">6、根据与您签订和履行相关协议或其他书面文件所必需的</span>
</p>
<p><span style="color: rgb(51, 51, 51);">7、法律法规等规定的其他情形。</span></p>
<p><span style="color: rgb(51, 51, 51);">请知悉,根据适用的法律,若我们对个人信息采取技术措施和其他必要措施进行处理,使得数据接收方无法重新识别特定个人且不能复原,则此类处理后数据的共享、转让、公开披露无需另行向您通知并征得您的同意。</span>
</p>
<p><strong>五、我们如何存储和保护您的个人信息</strong></p>
<p><strong>(一)个人信息的存储</strong></p>
<p><strong>1、存储地点</strong><span style="color: rgb(51, 51, 51);">:我们会按照法律法规规定,将境内收集的用户个人信息存储于中华人民共和国境内。目前我们不会跨境传输或存储您的个人信息或向境外提供个人信息的场景。将来如需跨境传输或存储的,我们会单独向您明确告知信息出境的目的、接收方、使用方式和范围、安全保证措施等情况并征得您的同意。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2、</span><strong>存储期限:除非依据法律法规或双方约定,我们只会在为实现服务目的所必需的最短时间内留存您的个人信息,此外,我们或对您的相关信息保存至相关法律所规定的必要期限(例如,根据《电子商务法》规定:商品和服务信息、交易信息保存时间自交易完成之日起不少于三年;根据《网络安全法》规定:采取监测、记录网络运行状态、网络安全事件的技术措施,并按照规定留存相关的网络日志不少于六个月;根据《互联网用户公众账号信息服务管理规定》:互联网用户公众账号信息服务提供者应当记录互联网用户公众账号信息服务使用者发布内容和日志信息,并按规定留存不少于六个月)。</strong>
</p>
<p><strong>我们在判断前述期限的标准主要包括并以其中较长者为准:</strong></p>
<p><strong>A. 完成与您相关的服务目的、维护相应服务及业务记录、应对您可能的查询或投诉;</strong></p>
<p><strong>B. 保证我们为您提供服务的安全和质量;</strong></p>
<p><strong>C. 您是否同意更长的留存期限;</strong></p>
<p><strong>D. 是否存在保留期限的其他特别约定。</strong></p>
<p>
<strong>在超出保存期限后,我们会根据适用法律的要求删除或者匿名化处理您的个人信息,但法律有特殊要求的除外。</strong>
</p>
<p>
<strong>当我们的产品或服务发生停止运营的情况时,我们将以推送通知、公告等形式通知您,并在合理期限内删除您的个人信息或进行匿名化处理,法律法规另有规定的除外。</strong>
</p>
<p><strong>(二)个人信息的保护措施</strong></p>
<p><span style="color: rgb(51, 51, 51);">数据传输方面采用传输层安全协议等密码技术通过Https等方式防止传输链路被嗅探、窃听截取风险建立安全的隐私数据采集环境保证数据采集的私密性和完整性。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">数据存储方面我们会通过安全的方式存储您的信息包括本地存储例如利用APP进行数据缓存。我们只会在为实现服务目的所必需的时间内或法律法规规定的条件下存储您的个人信息。您可以自主选择删除观看历史、视频缓存、搜索记录等您在使用</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音</span><span
style="color: rgb(51, 51, 51);">APP服务时提供和产生的数据记录。</span></p>
<p><span style="color: rgb(51, 51, 51);">信息安全方面,我们努力为您的信息安全提供保障,以防止信息的泄露、丢失、不当使用、未经授权访问和披露等。我们使用多方位的安全保护措施,以确保您的个人信息保护处于合理的安全水平,包括技术保护手段、管理制度控制、安全体系保障等诸多方面。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">数据访问和使用的安全控制方面,实施严格的数据权限控制机制,采取多重身份认证技术,并对能够处理您的信息的行为进行监控,避免数据被违规访问和未授权使用。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">建立完整的审计机制,对数据生命周期的全流程进行监控与审计,防止您的个人信息遭遇未经授权的访问、公开披露、使用、修改、人为或意外的损坏或丢失。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">其他可行的安全组织和管理措施:</span></p>
<p><span style="color: rgb(51, 51, 51);">1、在我们从第三方间接收集您的个人信息前我们会明确以书面形式如合作协议、承诺书要求该第三方在已经取得您明示同意后收集以及处理如共享等个人信息在书面协议层面要求第三方对个人信息来源的合法性和合规性作出承诺如第三方有违反行为的我们会明确要求对方承担相应法律责任</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">2、在我们向业务合作伙伴共享您的个人信息前我们会严格要求合作伙伴的信息保护义务与责任并要求业务合作伙伴在合作前需与</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">签署关于数据安全的保护协议,一旦业务合作伙伴有任何违反协议的行为,将须承担相应法律责任;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">3、我们建立了合理有效、科学健全的安全应急响应体系明确了网络安全事件的分类分级标准、安全应急响应组织及职责并根据安全事件的不同类型和级别制定了适当的应急响应预案明确规范了安全事件的报告流程和应急响应处置流程。同时我们还具备“</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">”安全应急响应中心,由专业的安全技术和运营团队负责维护,便于及时有效的响应、处置各类安全漏洞和突发事件,联合相关部门进行安全事件溯源和打击;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">4、如不幸发生个人信息安全事件的我们将按照法律法规的要求及时向您告知安全事件的基本情况和可能的影响、我们已采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施等。我们将及时将事件相关情况以邮件、信函、电话、推送通知等方式告知您难以逐一告知个人信息主体时我们会采取合理、有效的方式发布公告。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">5、请您知悉并理解互联网并非绝对安全的环境我们强烈建议通过安全方式、使用复杂密码协助我们保证您的账号安全。如发现自己的个人信息泄密尤其是您自己的账户或密码发生泄露请立即根据本政策文末中提供的联系方式联络我们以便我们采取相应措施来保护您的个人信息安全。</span>
</p>
<p><strong>六、您如何管理您的个人信息</strong></p>
<p><span style="color: rgb(51, 51, 51);">您对您的个人信息享有以下权利:</span></p>
<p><strong>(一)访问权</strong></p>
<p><strong>1、账号信息</strong><span style="color: rgb(51, 51, 51);">:您可以通过相关产品页面随时登陆您的个人账号,随时查询或访问您的账号中个人资料信息,包括:头像、昵称、星座、城市等。例如:“头像/昵称”信息在</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">APP中的访问路径为我的—头像栏</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">微信小程序的访问路径为:我的-&gt;点击个人头像-&gt;点击头像右下角编辑icon可进入资料编辑页面。</span>
</p>
<p><strong>2、使用信息</strong><span style="color: rgb(51, 51, 51);">:您可以通过相关产品页面随时查阅您的使用信息,包括:订单记录、浏览记录、动态记录等。例如:“浏览记录”信息在</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">APP和微信小程序中的访问路径为我的—足迹</span></p>
<p><span style="color: rgb(51, 51, 51);">3、财产信息您可以通过</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">APP 我的-钱包,以及</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">微信公众号-钱包查阅;</span></p>
<p><span style="color: rgb(51, 51, 51);">4、</span><strong>其他信息:您也可以在【我的】-【帮助与反馈】-【个人信息收集清单】中集中查询您的网络身份标识和鉴权信息、身份信息、用户基本资料、个人财产信息、用户使用过程信息、设备信息、内容制作与发布。如您在访问过程中遇到操作问题或如需获取其他前述方法无法获知的个人信息内容,您可通过本政策第九节提供的方式联系我们,我们将在核实您的身份后在合理期限内向您提供,但法律法规另有规定或本政策另有约定的除外。</strong>
</p>
<p><em>我们会按照如下规则进行个性化推荐或定向推送,并为您提供相应的退出机制:</em></p>
<p><em>我们会根据您的喜欢为您提供个性化的推广内容,</em></p>
<p><em>例如在主页推荐房间、派对 -
推荐;您可以通过“我的-设置-个性化推荐”-关闭。关闭后,您看到的相关内容以房间活跃值默认展示。</em></p>
<p><strong>(二)复制权</strong></p>
<p><strong>若您需要复制我们收集的关于你本人的基本资料、身份信息的,您可通过本政策第九节提供的方式联系我们,我们将在核实您的身份后在合理期限内向您提供,或在技术可行的前提下根据您的请求将您的个人信息转移至指定的个人信息处理者,但法律法规另有规定或本政策另有约定的除外。</strong>
</p>
<p><strong>(三)更正/修改权</strong></p>
<p>
<strong>若您发现您提供给我们的个人信息存在登记错误、不完整或有更新的,您可在我们产品和/或服务中更正/修改您的相关个人信息。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">1、对于您的部分个人信息您可以按照相关功能页面的指引和设置在线进行更正/修改。例如“头像/昵称”信息更正/修改路径为:我的—头像栏—编辑资料;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2、对于您在行使上述权利过程中遇到的困难或者其他可能目前无法向您提供在线自行更正/修改服务的,经过对您身份的验证,且更正/修改不影响信息的客观性和准确性的情况下,您</span><strong>有权对错误或不完整的信息作出更正或修改,或在特定情况下,尤其是数据错误时,通过我们公布的反馈与报错等措施将您的更正/修改申请提交给我们,或联系我们(联系方式:详见下文第九节),要求我们更正或修改您的数据,但法律法规另有规定的除外。但出于安全性和身份识别的考虑,您可能无法修改注册时提交的某些初始注册信息。</strong>
</p>
<p><strong>(四)删除权</strong></p>
<p><span
style="color: rgb(51, 51, 51);">1、对于您提供的部分个人信息您可以自行通过我们提供的相关产品和服务的功能页面主动删除您提供信息。例如</span><strong>头像/昵称”信息在羽声语音APP中的删除路径为【我的-个人主页-资料】。您也可以自主删除您发布的视频、动态、图片等。一旦您删除后,我们即会对此类信息进行删除或匿名化处理,法律法规另有规定的除外</strong><span
style="color: rgb(51, 51, 51);"></span></p>
<p><span
style="color: rgb(51, 51, 51);">2、在以下情形中</span><strong>您可以通过本政策第九节提供的联系方式</strong><span
style="color: rgb(51, 51, 51);">向我们提出删除个人信息的请求,但已做匿名化处理或法律法规另有规定的除外:</span>
</p>
<p><span style="color: rgb(51, 51, 51);">1如果我们处理个人信息的行为违反法律法规</span></p>
<p><span
style="color: rgb(51, 51, 51);">2如果我们收集、使用您的个人信息却未征得您的明确同意</span>
</p>
<p><span style="color: rgb(51, 51, 51);">3如果我们处理个人信息的行为严重违反了该政策</span></p>
<p><span style="color: rgb(51, 51, 51);">4如果您主动注销了账号</span></p>
<p><span style="color: rgb(51, 51, 51);">5当我们终止服务及运营时。</span></p>
<p><strong>(五)撤回同意权</strong></p>
<p><span style="color: rgb(51, 51, 51);">1、我们提供的产品或服务的部分功能需要获得您使用设备的相关权限包括位置、相机、麦克风等详见本隐私政策第二节第十二条“我们获取的设备权限”具体以产品实际获取的功能为准</span><strong>您可以在授权后随时撤回(或停止)对该权限的继续授权</strong><span
style="color: rgb(51, 51, 51);">,包括通过您设备中的设置页面或</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音</span><span
style="color: rgb(51, 51, 51);">APP的相关路径“我的-设置-隐私-隐私权限设置”)撤回相关授权。示例路径如下:</span>
</p>
<p>
<strong>1位置的权限您可通过【我的-设置-隐私-隐私权限设置-允许羽声语音直播访问位置信息】撤回同意;</strong>
</p>
<p>
<strong>2相机的权限您可通过【我的-设置-隐私-隐私权限设置-允许羽声语音直播访问相机】撤回同意;</strong>
</p>
<p>
<strong>3麦克风的权限您可通过【我的-设置-隐私-隐私权限设置-允许羽声语音直播访问相机】撤回同意;</strong>
</p>
<p><strong>4日历的权限您可通过【我的-系统设置-系统权限管理】撤回同意。</strong></p>
<p><span style="color: rgb(51, 51, 51);">2、</span><strong>就我们通过短信的方式向您发送的商业性服务信息如果您不想收到此类信息您可以通过联系我们联系方式详见下文第九节或者通过编辑“T”或“TD”并回复短信进行退订提请注意我们不会就您申请退订额外收取任何费用但可能会发生一定的通信或者网络费用具体以通信运营商与您约定为准退订成功后我们将不再向您发送此类型的短信。就我们通过您设备的系统通知向你推送的内容如果您不想收到此类信息你可选择在设备中关闭羽声语音直播的通知功能</strong><span
style="color: rgb(51, 51, 51);"></span></p>
<p><span style="color: rgb(51, 51, 51);">3、对于合作方使用或我们转让、公开披露您的个人信息您可以通过我们的相关功能页面或联系我们、合作方撤回您的授权但法律法规另有规定或本隐私政策另有约定的除外。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">4、您可以通过卸载</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音</span><span
style="color: rgb(51, 51, 51);">App的方式撤回我们通过当前设备对您的个人信息的收集能力。</span>
</p>
<p><strong>5、当您收回同意后我们将不再处理相应的个人信息。但您收回同意的决定不会影响此前基于您的授权而开展的个人信息处理。当您更新APP版本后未经您的明确同意我们不会更改您之前设置的权限状态。</strong>
</p>
<p>
<strong>6、对于您无法直接通过上述路径改变授权同意或撤回同意的您均可通过本政策第九节提供的方式联系我们进行处理但法律法规另有规定或本政策另有约定的除外。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">(六)注销权</span></p>
<p><span
style="color: rgb(51, 51, 51);">我们向您提供账户注销的途径。在您符合国家相关法律法规规定及约定注销条件的情况下,可以通过本隐私政策预留的联系方式(</span><strong>详见下文第九节</strong><span
style="color: rgb(51, 51, 51);">)或在线申请注销您的账户。</span></p>
<p><strong>提请注意:当您注销账号后,您将无法再以该账号登录和使用我们的产品与服务;且该账号在羽声语音及旗下的其他产品与服务使用期间已产生的但未消耗完毕的权益及未来的预期利益等全部权益将被清除;该账号下的内容、信息、数据、记录等将会被删除或匿名化处理(但法律法规另有规定或监管部门另有要求的除外,如依据《中华人民共和国网络安全法》规定,您的网络操作日志将至少保留六个月的时间);羽声语音账号注销完成后,将无法恢复。更多关于羽声语音账号注销的流程、条件等事项请详见《帐号注销须知》。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">如您在谨慎考虑后仍执意决定注销您的</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">账号的,您可以在您使用的我们的产品/服务的相关功能设置页面或根据操作指引向我们提交注销申请,</span><strong>羽声语音APP的在线申请注销路径为【我的】-【设置】-【账号与安全】-【安全中心】-【注销账号】羽声语音直播微信小程序可以选择通过上述羽声语音APP注销路径、联系客服或者通过本隐私政策第九节预留的联系方式申请注销。</strong>
</p>
<p>
<strong>在您主动注销账户之后,我们将停止为您提供产品或服务,根据适用法律的要求删除您的个人信息,或使其匿名化处理。</strong>
</p>
<p><strong>(七)约束信息系统自动决策</strong></p>
<p><span style="color: rgb(51, 51, 51);">在某些业务功能中,我们可能仅依据信息系统、算法等在内的非人工自动决策机制做出决定。如果这些决定显著影响您的合法权益,您有权通过本政策第九节提供的联系方式要求我们做出解释并拒绝我们仅通过自动化决策的方式作出决定,我们也将在不侵害</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音直播</span><span
style="color: rgb(51, 51, 51);">商业秘密或其他用户权益、社会公共利益、国家利益的前提下做出合理解释、处理。</span>
</p>
<p><span style="color: rgb(51, 51, 51);"></span><strong>八)我们对去世用户的个人信息保护</strong>
</p>
<p><strong>1.
羽声语音直播将根据《个人信息保护法》的相关规定保护死者个人信息。羽声语音用户(仅限自然人)去世后,其近亲属为了自身的合法、正当利益,可以通过本隐私政策第九节“联系我们”中公示的联系方式,对去世用户的相关个人信息行使查阅、复制、更正、删除等权利,但是去世用户生前另有安排的除外。</strong>
</p>
<p><strong>2.
您理解并确认,为了充分保护去世用户的个人信息权益,申请行使本条权利的去世用户近亲属需要根据羽声语音直播的指定流程或客服提示,提交去世用户的身份证明文件、死亡证明文件、申请人的身份证明文件、申请人与去世用户的亲属关系证明文件,并提供申请行使的权利种类、目的。更多关于去世用户的个人信息保护流程、条件等事项请联系在线客服。</strong>
</p>
<p><strong>(九)响应您的上述请求</strong></p>
<p>
<strong>如果您在行使上述权利、处置您的个人信息时有任何疑问,您均可以通过本隐私政策第九节中公示的联系方式与我们沟通解决。</strong>
</p>
<p>
<strong>为保障安全当您提出前述请求时您需要提供上述要求材料或以其他方式证明您的身份再处理您的请求若您拒绝提供相应证明材料我们将无法响应您的请求。我们将在15日内做出答复</strong><span
style="color: rgb(51, 51, 51);">。在以下情形中,按照法律法规要求,我们将无法响应您的请求:</span>
</p>
<p><span style="color: rgb(51, 51, 51);">1、与国家安全、国防安全、国家利益有关的</span></p>
<p><span style="color: rgb(51, 51, 51);">2、与公共安全、公共卫生、重大公共利益有关的</span></p>
<p><span style="color: rgb(51, 51, 51);">3、与犯罪刑事侦查、起诉、立案、审判和执行判决等有关的</span>
</p>
<p><span style="color: rgb(51, 51, 51);">4、有充分证据表明个人信息主体存在主观恶意串通妨碍第三人行使合法权益或滥用权利使其他个人、组织的合法权益受到严重损害的</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">5、响应您的请求将导致您或其他个人、组织的合法权益受到严重损害的</span>
</p>
<p><span style="color: rgb(51, 51, 51);">6、涉及商业秘密的。</span></p>
<p><strong>七、未成年人保护</strong></p>
<p><span style="color: rgb(51, 51, 51);">我们的产品、网站和服务主要面向成年人。</span><strong>您知悉,羽声语音直播将依赖用户提供的个人信息判断用户是否为未成年人</strong><span
style="color: rgb(51, 51, 51);">。若您是未成年人,建议您及您的监护人仔细阅读本政策,并在征得您的监护人同意的前提下使用我们的产品或服务或向我们提供信息。对于经父母或监护人同意使用我们的产品或服务而收集未成年人个人信息的情况,我们只会在法律法规允许、父母或监护人明确同意或者保护未成年人所必要的情况下使用、共享、转让或披露此信息。</span>
</p>
<p><strong>特别地若您是14周岁以下的儿童将适用《羽声语音儿童个人信息保护规则》建议您及您的监护人仔细阅读《羽声语音儿童个人信息保护规则》并在征得您的监护同意的前提下使用我们的产品或服务。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">若您是未成年人的监护人,一旦发现未成年人虚报年龄注册账号的,可通过本政策第九节所述方式向我们提出中止/终止该未成年人的账号或者要求向该未成年人提供青少年保护服务模式下的产品与/或服务。为了验证请求方为未成年人的监护人,特别是当您作为监护人提出资金类申请时,我</span><strong>们可能需要您向我们提供被监护人以及监护人的身份证(正面及反面)复印件/扫描件一份、可证明法定监护关系的有效法律文件(如出生证明、户口本)以及监护人手机号码、收款账号信息(如需),以保障用户的账号与资金安全。我们在收到您的上述文件后,会及时审核其真实性和有效性,并请确保监护人的手机畅通,以便我们的专业客服联系您进一步核实身份(如需)。同时,我们将依法记录、保存验证身份信息及验证结果</strong><span
style="color: rgb(51, 51, 51);">,这些信息仅供完成验证、争议解决服务之目的,或者其他法律法规所规定的用途,我们向您承诺,我们会以最大努力保障您的个人信息安全,未经您明示授权不会用作其他目的。以上信息包含您及您所监护的未成年人的个人敏感信息,您有权拒绝提供,但我们收集这些信息是为了调查事实与帮助您解决问题,如您拒绝提供上述信息,我们可能无法向您及时反馈投诉、申诉或咨询结果。</span>
</p>
<p><span style="color: rgb(51, 51, 51);">若您是未成年人的监护人,当您对您所监护的未成年人的个人信息保护有任何相关疑问时,请通过第九节“联系我们”中公示的联系方式与我们联系。</span>
</p>
<p><strong>八、本政策的更新</strong></p>
<p>
<strong>为保证您能够获得更好的服务并应平台业务调整以及未来发展,本政策也会与时俱进随之更新。但在更新的同时,我们会以单独、明显的方式获取您的授权。</strong>
</p>
<p>
<strong>我们在应用程序、官方网站更新版本后或官方发出公告后以合理、适当、明显的方式提醒您相关内容的更新,以便您能更快了解您权利的更迭。</strong>
</p>
<p>
<strong>对于重大变更,我们还会提供更为显著的通知方式(我们会通过包括但不限于邮件、短信、公告、首页或在浏览页面做特别提示等方式,来向您表达政策的更新内容)。</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">本政策所指的重大变更包括但不限于:</span></p>
<p><span
style="color: rgb(51, 51, 51);">(一)我们的服务模式发生重大变化。如处理个人信息的目的、处理的个人信息类型、个人信息的使用方式等;</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">(二)我们在所有权结构、组织架构等方面发生重大变化。如业务调整、破产并购等引起的所有者变更等;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">(三)个人信息共享、转让或公开披露的主要对象发生变化;</span>
</p>
<p><span
style="color: rgb(51, 51, 51);">(四)您参与个人信息处理方面的权利及其行使方式发生重大变化。</span>
</p>
<p><strong>九、联系我们</strong></p>
<p>
<strong>如您对本政策或您个人信息的相关事宜有投诉、举报、意见或建议的可以通过如下方式与我们陕西启星汇申网络科技有限公司联系我们将在15天内予以您答复</strong>
</p>
<p><span style="color: rgb(51, 51, 51);">1、</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">羽声语音</span><span
style="color: rgb(51, 51, 51);">站内客服:【我的】-【在线客服】-输入“人工客服”进行咨询;</span>
</p>
<p><span style="color: rgb(51, 51, 51);">2、人工客服电话</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">17531991222</span></p>
<p><span style="color: rgb(51, 51, 51);">3、个人信息保护负责人电话</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">17531991222</span></p>
<p style="text-align: left;"><span
style="color: rgb(51, 51, 51);">4、我们还设立了个人信息保护专职部门您可以写信至</span><span
style="font-family: 宋体;">陕西省咸阳市高新技术产业开发区高科三路科技企业孵化园3栋4层</span><span
style="color: rgb(51, 51, 51);">法务合规部(收);邮编</span><span
style="color: rgb(51, 51, 51); font-family: 宋体;">710000</span></p>
<p><strong>十、其他</strong></p>
<p><strong>本政策的解释、执行及争议解决均适用中华人民共和国法律(不包括冲突法)。因本政策产生争议的,双方应首先友好协商,协商不成的,您同意通过被告所在地有管辖权的法院提起诉讼来寻求解决方案。</strong>
</p>
<p><strong>陕西启星汇申网络科技有限公司 </strong></p>
<p><strong>陕西省咸阳市高新技术产业开发区高科三路科技企业孵化园3栋4层</strong></p>
<p><span
style="color: rgb(26, 26, 26); background-color: rgb(255, 255, 255);">陕ICP备2024054760号-7A</span>
</p>
<p></p>
<p></p>
<p style="text-align: left;"><span style="font-family: 宋体;"> </span></p></body>
</html>

View File

@@ -1,8 +0,0 @@
package com.xscm.moduleutil;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
public class BaseEvent {
}

View File

@@ -1,373 +0,0 @@
package com.xscm.moduleutil.activity;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;
import com.alibaba.android.arouter.launcher.ARouter;
import com.blankj.utilcode.util.ActivityUtils;
import com.blankj.utilcode.util.BarUtils;
import com.blankj.utilcode.util.LogUtils;
import com.hjq.toast.ToastUtils;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.base.CommonAppContext;
import com.xscm.moduleutil.dialog.LoadingDialog;
import com.xscm.moduleutil.utils.BackgroundManager;
import com.xscm.moduleutil.utils.ColorManager;
import com.xscm.moduleutil.utils.DisplayUtil;
import com.xscm.moduleutil.widget.QXGiftDriftView;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.lang.reflect.Method;
public abstract class BaseAppCompatActivity<VDB extends ViewDataBinding> extends AppCompatActivity
implements BackgroundManager.BackgroundUpdateListener, ColorManager.ColorChangeListener {
@Override
protected void attachBaseContext(Context newBase) {
// 设置字体缩放比例为1.0f,即不跟随系统字体大小变化
super.attachBaseContext(DisplayUtil.attachBaseContext(newBase, 1.0f));
}
protected VDB mBinding;
@Subscribe (threadMode = ThreadMode.MAIN)
public void onEvent(Object event) {
}
private LoadingDialog mLoadingDialog;
// 添加广播接收器成员变量
private BroadcastReceiver mLogoutReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if ("com.xscm.moduleutil.ACTION_USER_LOGOUT".equals(intent.getAction())) {
// 在这里处理用户登出后的UI更新
// 例如:隐藏需要登录才能显示的控件
// 或者跳转到登录状态的页面
handleUserLogout();
}
}
};
// 处理用户登出的方法
protected void handleUserLogout() {
// 子类可以重写此方法处理具体的登出逻辑
// 例如更新UI状态、清除本地数据等
ActivityUtils.finishAllActivities();
}
QXGiftDriftView qxGiftDriftView;
protected void doDone(){}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
initFestivalTheme(CommonAppContext.getInstance().is_open);
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
// getWindow().getDecorView().setBackgroundResource(R.attr.app_bg_img);
setContentView(getLayoutId());
doDone();
// 隐藏标题栏
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
mBinding = DataBindingUtil.setContentView(this, getLayoutId());
mBinding.setLifecycleOwner(this);
ARouter.getInstance().inject(this);
BarUtils.setStatusBarLightMode(this, isLightMode());
BarUtils.transparentStatusBar(this);
initView();
initData();
initCompleted();
// 注册背景更新监听器
BackgroundManager.getInstance().addListener(this);
// 尝试加载网络背景
// loadNetworkBackground();
// 注册颜色变化监听器
ColorManager.getInstance().addColorChangeListener(this);
// 注册登出广播接收器
IntentFilter filter = new IntentFilter("com.xscm.moduleutil.ACTION_USER_LOGOUT");
registerReceiver(mLogoutReceiver, filter);
EventBus.getDefault().register(this);
}
// 节日判断+主题切换核心方法
private void initFestivalTheme(int currentFestival) {
switch (currentFestival) {
case 0:
setTheme(R.style.AppTheme_CustomAttrs);
break;
case 1:
setTheme(R.style.AppTheme_Spring_CustomAttrs);
break;
case 2:
setTheme(R.style.AppTheme_newYear_CustomAttrs);
break;
default:
// 默认皮肤
setTheme(R.style.AppTheme_CustomAttrs);
break;
}
}
// 在Activity中
private static final int REQUEST_OVERLAY_PERMISSION = 1001;
private void checkAndRequestOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
new AlertDialog.Builder(this)
.setTitle("需要悬浮窗权限")
.setMessage("应用需要悬浮窗权限才能显示飘屏效果")
.setPositiveButton("去设置", (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION);
})
.setNegativeButton("取消", null)
.show();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_OVERLAY_PERMISSION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(this)) {
ToastUtils.show("已获得悬浮窗权限");
} else {
ToastUtils.show("未获得悬浮窗权限");
}
}
}
}
@Override
public void onColorChanged() {
// 在主线程中更新UI
runOnUiThread(this::updateUIColors);
}
// 子类可以重写此方法来更新UI颜色
protected void updateUIColors() {
// 默认实现,子类可以覆盖
}
//在类中添加以下成员变量
private Handler timerHandler = new Handler();
private Runnable timerRunnable = new Runnable() {
@Override
public void run() {
// 调用你要执行的方法
// executePeriodicTask();
// 每10秒执行一次
timerHandler.postDelayed(this, 10000);
}
};
// 启动定时器的方法
private void startTimer() {
timerHandler.postDelayed(timerRunnable, 10000);
}
protected void loadNetworkBackground() {
// 只有当已经有背景URL时才加载
// int backgroundUrl = BackgroundManager.getInstance().getBackgroundUrl();
// getWindow().getDecorView().setBackgroundResource(backgroundUrl);
}
@Override
public void onBackgroundUpdated(Drawable drawable) {
// 当背景更新时更新当前Activity的背景
if (drawable != null) {
getWindow().getDecorView().setBackground(drawable);
}
}
// 提供一个方法供子类调用用于设置背景URL
protected void setNetworkBackgroundUrl(String url) {
// BackgroundManager.getInstance().setBackgroundUrl(url);
}
@Override
public Resources getResources() {//禁止app字体大小跟随系统字体大小调节
Resources resources = super.getResources();
if (resources != null && resources.getConfiguration().fontScale != 1.0f) {
android.content.res.Configuration configuration = resources.getConfiguration();
configuration.fontScale = 1.0f;
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
}
return resources;
}
static float fontScale = 1f;
// @Override
// public Resources getResources() {
// Resources resources = super.getResources();
// return DisplayUtil.getResources(this,resources,fontScale);
// }
public void setFontScale(float fontScale) {
this.fontScale = fontScale;
DisplayUtil.recreate(this);
}
public boolean isLightMode() {
return true;
}
protected abstract void initData();
protected abstract void initView();
protected void initCompleted() {
}
protected abstract int getLayoutId();
@Override
public void finish() {
super.finish();
LogUtils.e(this.getComponentName()+"========finish");
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
// 移除背景更新监听器
BackgroundManager.getInstance().removeListener(this);
// 移除颜色变化监听器
ColorManager.getInstance().removeColorChangeListener(this);
if (mBinding != null) {
mBinding.unbind();
}
try {
unregisterReceiver(mLogoutReceiver);
} catch (Exception e) {
// 忽略异常
}
try {
unregisterReceiver(mLogoutReceiver);
} catch (Exception e) {
// 忽略异常
}
super.onDestroy();
}
public void showLoading(String content) {
if (mLoadingDialog == null) {
mLoadingDialog = new LoadingDialog(this,content);
}
if (!mLoadingDialog.isShowing()) {
mLoadingDialog.show();
}
}
public void showLoading() {
if (mLoadingDialog == null) {
mLoadingDialog = new LoadingDialog(this);
}
if (!mLoadingDialog.isShowing()) {
mLoadingDialog.show();
}
}
public void disLoading() {
if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
mLoadingDialog.dismiss();
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
View view = getCurrentFocus();
if (isShouldHideInput(view, ev)) {
InputMethodManager Object = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (Object != null) {
Object.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
}
try {
return super.dispatchTouchEvent(ev);
} catch (Exception e) {
return false;
}
}
//判断是否隐藏键盘
public boolean isShouldHideInput(View v, MotionEvent event) {
if (v != null && (v instanceof EditText)) {
int[] leftTop = {0, 0};
//获取输入框当前的location位置
v.getLocationInWindow(leftTop);
int left = leftTop[0];
int top = leftTop[1];
int bottom = top + v.getHeight();
int right = left + v.getWidth();
if (event.getX() > left && event.getX() < right
&& event.getY() > top && event.getY() < bottom) {
// 点击的是输入框区域保留点击EditText的事件
return false;
} else {
return true;
}
}
return false;
}
@Override
protected void onResume() {
super.onResume();
LogUtils.e(this.getComponentName()+"========onResume");
}
@Override
protected void onPause() {
super.onPause();
LogUtils.e(this.getComponentName()+"=========onPause");
}
@Override
protected void onStop() {
super.onStop();
LogUtils.e(this.getComponentName()+"=========onStop");
}
}

View File

@@ -1,209 +0,0 @@
package com.xscm.moduleutil.adapter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.bean.GiftPackBean;
import com.xscm.moduleutil.event.RoomGiftPackToEvent;
import com.xscm.moduleutil.utils.ImageUtils;
import org.greenrobot.eventbus.EventBus;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* @Author lxj$
* @Time 2025年7月29日23:36:25$ $
* @Description 背包礼物适配器$
*/
public class GiftPackAdapter extends BaseQuickAdapter<GiftPackBean, BaseViewHolder> {
private final List<GiftPackBean> mDatas;
private final LayoutInflater inflater;
private final Context mContext;
private final MyGestureDetector gestureDetector;
private final String type;
/**
* 页数下标,从0开始(当前是第几页)
*/
private final int curIndex;
/**
* 每一页显示的个数
*/
private final int pageSize = 100;
public GiftPackAdapter(Context context, List<GiftPackBean> mDatas, int curIndex, String type) {
super(R.layout.item_gift_room, mDatas);
inflater = LayoutInflater.from(context);
this.mDatas = mDatas;
this.curIndex = curIndex;
this.mContext = context;
this.type = type;
this.gestureDetector = new MyGestureDetector(mContext);
}
/**
* 先判断数据集的大小是否足够显示满本页mDatas.size() > (curIndex+1)*pageSize,
* 如果够则直接返回每一页显示的最大条目个数pageSize,
* 如果不够,则有几项返回几,(mDatas.size() - curIndex * pageSize);(也就是最后一页的时候就显示剩余item)
*/
// @Override
// public int getCount() {
// return mDatas.size() > (curIndex + 1) * pageSize ? pageSize : (mDatas.size() - curIndex * pageSize);
// }
//
// @Override
// public GiftPackBean getItem(int position) {
// return mDatas.get(position + curIndex * pageSize);
// }
@Override
protected void convert(@NonNull BaseViewHolder helper, GiftPackBean giftModel) {
helper.getView(R.id.cl_gift).setOnClickListener(v -> {
// RoonGiftModel clickedModel = (RoonGiftModel) v.getTag();
EventBus.getDefault().post(new RoomGiftPackToEvent(this, giftModel, 2));
});
helper.setVisible(R.id.integral, true).setText(R.id.integral, "x" + giftModel.getNum());
helper.setText(R.id.tv_gift_name, giftModel.getGift_name());
//设置礼物价格
String surplusTxt = giftModel.getGift_price();
SpannableStringBuilder stringBuilder = new SpannableStringBuilder(surplusTxt);
//ForegroundColorSpan 为文字前景色BackgroundColorSpan为文字背景色
ForegroundColorSpan redSpan = new ForegroundColorSpan(mContext.getResources().getColor(R.color.color_FFA9A9A9));
stringBuilder.setSpan(redSpan, surplusTxt.length(), surplusTxt.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);//修改最后两个字体的颜色
helper.setText(R.id.tv_gift_price,stringBuilder);
//加载礼物图片
ImageUtils.loadImageView(giftModel.getBase_image(),helper.getView(R.id.iv_gift_pic));
//设置选中后的样式
if (giftModel.isChecked()) {//被选中
helper.getView(R.id.cl_iv_down_on).setBackgroundResource(R.mipmap.room_gift_bjx);
helper.setVisible(R.id.iv_down_on, false);
} else {
helper.setVisible(R.id.iv_down_on, false);
helper.getView(R.id.cl_iv_down_on).setBackgroundResource(0);
}
}
@Override
public long getItemId(int position) {
return position + (long) curIndex * pageSize;
}
private static class MyGestureDetector extends GestureDetector {
private static WeakReference<GiftPackAdapter> sAdapter = new WeakReference<>(null);
private static GiftPackBean sGiftModel;
private GiftPackAdapter mAdapter;
private GiftPackBean mGiftModel;
public void setGiftModel(GiftPackAdapter adapter, GiftPackBean gift) {
sAdapter = new WeakReference<>(adapter);
sGiftModel = gift;
}
private static final SimpleOnGestureListener sSimpleOnGestureListener = new SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
EventBus.getDefault().post(new RoomGiftPackToEvent(sAdapter.get(), sGiftModel, 1));
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
EventBus.getDefault().post(new RoomGiftPackToEvent(sAdapter.get(), sGiftModel, 2));
return true;
}
};
public MyGestureDetector(Context context) {
super(context, sSimpleOnGestureListener);
setOnDoubleTapListener(sSimpleOnGestureListener);
}
}
// @Override
// @SuppressLint({"SetTextI18n", "ClickableViewAccessibility"})
// public View getView(int position, View convertView, ViewGroup parent) {
// GiftPackAdapter.ViewHolder viewHolder;
// GiftPackBean giftModel = getItem(position);
// if (convertView == null) {
// convertView = inflater.inflate(R.layout.item_gift_room, parent, false);
// viewHolder = new GiftPackAdapter.ViewHolder();
// viewHolder.tv_gift_name = (TextView) convertView.findViewById(R.id.tv_gift_name);
// viewHolder.tv_gift_price = (TextView) convertView.findViewById(R.id.tv_gift_price);
// viewHolder.iv_gift_pic = (ImageView) convertView.findViewById(R.id.iv_gift_pic);
// viewHolder.item_layout = (ConstraintLayout) convertView.findViewById(R.id.cl_gift);
// viewHolder.ivDownOn = (ImageView) convertView.findViewById(R.id.iv_down_on);
// viewHolder.cl_iv_down_on = (ConstraintLayout) convertView.findViewById(R.id.cl_iv_down_on);
// viewHolder.integral = (TextView) convertView.findViewById(R.id.integral);
// viewHolder.im_heart = (ImageView) convertView.findViewById(R.id.im_heartssss);
// convertView.setTag(viewHolder);
// } else {
// viewHolder = (GiftPackAdapter.ViewHolder) convertView.getTag();
// }
//
// viewHolder.item_layout.setOnClickListener(v -> {
//// RoonGiftModel clickedModel = (RoonGiftModel) v.getTag();
// EventBus.getDefault().post(new RoomGiftPackToEvent(this, giftModel, 2));
//
// });
// viewHolder.integral.setVisibility(View.VISIBLE);
// viewHolder.integral.setText("x"+giftModel.getNum());
// //设置礼物名字
// viewHolder.tv_gift_name.setText(giftModel.getGift_name());
// //设置礼物价格
// String surplusTxt = giftModel.getGift_price();
// SpannableStringBuilder stringBuilder = new SpannableStringBuilder(surplusTxt);
// //ForegroundColorSpan 为文字前景色BackgroundColorSpan为文字背景色
// ForegroundColorSpan redSpan = new ForegroundColorSpan(mContext.getResources().getColor(R.color.color_FFA9A9A9));
// stringBuilder.setSpan(redSpan, surplusTxt.length(), surplusTxt.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);//修改最后两个字体的颜色
// viewHolder.tv_gift_price.setText(stringBuilder);
//// viewHolder.item_layout.setTag(R.id.id_gift_tag, giftModel);
//
// //加载礼物图片
// ImageUtils.loadImageView(giftModel.getBase_image(), viewHolder.iv_gift_pic);
// //设置选中后的样式
//
// if (giftModel.isChecked()) {//被选中
// viewHolder.cl_iv_down_on.setBackgroundResource(R.mipmap.room_gift_bjx);
// viewHolder.ivDownOn.setVisibility(View.GONE);
// } else {
// viewHolder.ivDownOn.setVisibility(View.GONE);
// viewHolder.cl_iv_down_on.setBackgroundResource(0);
// }
//
// return convertView;
// }
//
//
// static class ViewHolder {
// public ConstraintLayout item_layout;
// public TextView tv_gift_name, tv_gift_price, integral;
// public ImageView iv_gift_pic;
// public TextView tv_gift_change_love_values;
// public ImageView ivDownOn;
// public ConstraintLayout cl_iv_down_on;
// public ImageView im_heart;
// }
}

View File

@@ -1,324 +0,0 @@
package com.xscm.moduleutil.adapter;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.blankj.utilcode.util.LogUtils;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.hjq.toast.ToastUtils;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.bean.RoonGiftModel;
import com.xscm.moduleutil.event.RoomGiftClickToEvent;
import com.xscm.moduleutil.utils.ImageUtils;
import org.greenrobot.eventbus.EventBus;
import java.lang.ref.WeakReference;
import java.util.List;
public class GiftRoomAdapter extends BaseQuickAdapter<RoonGiftModel, BaseViewHolder> {
private final List<RoonGiftModel> mDatas;
private final LayoutInflater inflater;
private final Context mContext;
private final MyGestureDetector gestureDetector;
private final String type;
/**
* 页数下标,从0开始(当前是第几页)
*/
private final int curIndex;
/**
* 每一页显示的个数
*/
private final int pageSize = 100;
public GiftRoomAdapter(Context context, List<RoonGiftModel> mDatas, int curIndex, String type) {
super(R.layout.item_gift_room);
this.mDatas = mDatas;
this.curIndex = curIndex;
this.mContext = context;
this.type = type;
this.gestureDetector = new MyGestureDetector(mContext);
inflater = LayoutInflater.from(mContext);
}
/**
* 先判断数据集的大小是否足够显示满本页mDatas.size() > (curIndex+1)*pageSize,
* 如果够则直接返回每一页显示的最大条目个数pageSize,
* 如果不够,则有几项返回几,(mDatas.size() - curIndex * pageSize);(也就是最后一页的时候就显示剩余item)
*/
// @Override
// public int getCount() {
// return mDatas !=null ? mDatas.size() : 0;
//// return mDatas.size() > (curIndex + 1) * pageSize ? pageSize : (mDatas.size() - curIndex * pageSize);
// }
//
// @Override
// public RoonGiftModel getItem(int position) {
// return mDatas.get(position);
//// return mDatas.get(position + curIndex * pageSize);
// }
@Override
protected void convert(@NonNull BaseViewHolder helper, RoonGiftModel giftModel) {
helper.getView(R.id.cl_gift).setOnClickListener(v -> {
// RoonGiftModel clickedModel = (RoonGiftModel) v.getTag();
if (giftModel.getIs_lock() == 0) {
EventBus.getDefault().post(new RoomGiftClickToEvent(this, giftModel, 1));
} else if (giftModel.getIs_lock() == 1) {
ToastUtils.show("当前属于爵位礼物,请开通爵位");
}
});
if (giftModel.getIs_lock() == 0) {
helper.setVisible(R.id.iv_gift_select,false);
} else {
helper.setVisible(R.id.iv_gift_select,true);
}
if (TextUtils.isEmpty(giftModel.getIcon())) {
helper.setVisible(R.id.im_heartssss,false);
}else {
helper.setVisible(R.id.im_heartssss,true);
ImageUtils.loadHead(giftModel.getIcon(), helper.getView(R.id.im_heartssss));
}
//设置礼物名字
helper.setText(R.id.tv_gift_name,giftModel.getGift_name());
if (TextUtils.isEmpty(giftModel.getNum())){
helper.setVisible(R.id.integral, false);
}else {
helper.setVisible(R.id.integral, true).setText(R.id.integral, "x" + giftModel.getNum());
}
//设置礼物价格
String surplusTxt = giftModel.getGift_price();
SpannableStringBuilder stringBuilder = new SpannableStringBuilder(surplusTxt);
//ForegroundColorSpan 为文字前景色BackgroundColorSpan为文字背景色
ForegroundColorSpan redSpan = new ForegroundColorSpan(mContext.getResources().getColor(R.color.color_FFA9A9A9));
stringBuilder.setSpan(redSpan, surplusTxt.length(), surplusTxt.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);//修改最后两个字体的颜色
helper.setText(R.id.tv_gift_price,stringBuilder);
// viewHolder.item_layout.setTag(R.id.id_gift_tag, giftModel);
//加载礼物图片
ImageUtils.loadImageView(giftModel.getBase_image(), helper.getView(R.id.iv_gift_pic));
//设置选中后的样式
if (giftModel.getGift_bag() == 10) {
helper.setText(R.id.tv_gift_name,"");
helper.getView(R.id.cl_gift).setBackgroundResource(R.mipmap.gift_tkzj);
helper.getView(R.id.tv_gift_name).setBackgroundResource(R.mipmap.gift_name_tkzj);
} else if (giftModel.getGift_bag() == 11) {
helper.setText(R.id.tv_gift_name,"");
helper.getView(R.id.cl_gift).setBackgroundResource(R.mipmap.gift_syzc);
helper.getView(R.id.tv_gift_name).setBackgroundResource(R.mipmap.gift_name_syzc);
} else if (giftModel.getGift_bag() == 12) {
helper.setText(R.id.tv_gift_name,"");
helper.getView(R.id.cl_gift).setBackgroundResource(R.mipmap.gift_sjzd);
helper.getView(R.id.tv_gift_name).setBackgroundResource(R.mipmap.gift_name_skzd);
}else {
helper.setText(R.id.tv_gift_name,giftModel.getGift_name());
helper.getView(R.id.tv_gift_name).setBackgroundResource(0);
helper.getView(R.id.cl_gift).setBackgroundResource(R.mipmap.gift_bj);
}
if (giftModel.isChecked()) {//被选中
helper.getView(R.id.cl_iv_down_on).setBackgroundResource(R.mipmap.room_gift_bjx);
helper.setVisible(R.id.iv_down_on,false);
} else {
helper.getView(R.id.cl_iv_down_on).setBackgroundResource(0);
helper.setVisible(R.id.iv_down_on,false);
}
}
@Override
public long getItemId(int position) {
return position;
// return position + (long) curIndex * pageSize;
}
private static class MyGestureDetector extends GestureDetector {
private static WeakReference<GiftRoomAdapter> sAdapter = new WeakReference<>(null);
private static RoonGiftModel sGiftModel;
private GiftRoomAdapter mAdapter;
private RoonGiftModel mGiftModel;
public void setGiftModel(GiftRoomAdapter adapter, RoonGiftModel gift) {
sAdapter = new WeakReference<>(adapter);
sGiftModel = gift;
}
private static final SimpleOnGestureListener sSimpleOnGestureListener = new SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
EventBus.getDefault().post(new RoomGiftClickToEvent(sAdapter.get(), sGiftModel, 1));
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
EventBus.getDefault().post(new RoomGiftClickToEvent(sAdapter.get(), sGiftModel, 2));
return true;
}
};
public MyGestureDetector(Context context) {
super(context, sSimpleOnGestureListener);
setOnDoubleTapListener(sSimpleOnGestureListener);
}
}
public void updateData(List<RoonGiftModel> newData) {
this.mDatas.clear();
// 确保新数据都不处于选中状态
for (RoonGiftModel model : newData) {
model.setChecked(false);
}
this.mDatas.addAll(newData);
}
// @Override
// @SuppressLint({"SetTextI18n", "ClickableViewAccessibility"})
// public View getView(int position, View convertView, ViewGroup parent) {
// ViewHolder viewHolder;
// RoonGiftModel giftModel = getItem(position);
// if (convertView == null) {
// convertView = inflater.inflate(R.layout.item_gift_room, parent, false);
// viewHolder = new ViewHolder();
// viewHolder.item_layout = (ConstraintLayout) convertView.findViewById(R.id.cl_gift);
// viewHolder.cl_iv_down_on = (ConstraintLayout) convertView.findViewById(R.id.cl_iv_down_on);
// viewHolder.tv_gift_name = (TextView) convertView.findViewById(R.id.tv_gift_name);
// viewHolder.tv_gift_price = (TextView) convertView.findViewById(R.id.tv_gift_price);
// viewHolder.iv_gift_pic = (ImageView) convertView.findViewById(R.id.iv_gift_pic);
// viewHolder.ivDownOn = (ImageView) convertView.findViewById(R.id.iv_down_on);
// viewHolder.iv_gift_select = (ImageView) convertView.findViewById(R.id.iv_gift_select);
//
// // im_heart现在位于顶层布局中
// viewHolder.im_heart = (ImageView) convertView.findViewById(R.id.im_heartssss);
// convertView.setTag(viewHolder);
// } else {
// viewHolder = (ViewHolder) convertView.getTag();
// }
//
// viewHolder.item_layout.setOnClickListener(v -> {
//// RoonGiftModel clickedModel = (RoonGiftModel) v.getTag();
// if (giftModel.getIs_lock() == 0) {
// EventBus.getDefault().post(new RoomGiftClickToEvent(this, giftModel, 1));
// } else if (giftModel.getIs_lock() == 1) {
// ToastUtils.show("当前属于爵位礼物,请开通爵位");
// }
// });
// if (giftModel.getIs_lock() == 0) {
// viewHolder.iv_gift_select.setVisibility(GONE);
// } else {
// viewHolder.iv_gift_select.setVisibility(VISIBLE);
// }
// if (TextUtils.isEmpty(giftModel.getIcon())) {
// viewHolder.im_heart.setVisibility(GONE);
// }else {
// viewHolder.im_heart.setVisibility(VISIBLE);
// ImageUtils.loadHead(giftModel.getIcon(), viewHolder.im_heart);
// }
//
//// if (giftModel.getIs_cp() == 0 && giftModel.getIs_teacher() == 0 ) {//这是cp礼物
//// viewHolder.im_heart.setVisibility(GONE);
//// } else {
//// if (viewHolder.im_heart != null) {
//// viewHolder.im_heart.setVisibility(VISIBLE);
//// if (giftModel.getIs_cp() == 1) {
//// viewHolder.im_heart.setImageResource(R.mipmap.icon_heart);
//// }else if (giftModel.getIs_teacher() == 1) {
//// viewHolder.im_heart.setImageResource(R.mipmap.icon_teacher);
//// }
//// }
//// }
//
//
// /*
// * 在给View绑定显示的数据时计算正确的position = position + curIndex * pageSize
// */
//// viewHolder.tv_gift_num.setVisibility(type.equals("1") ? View.VISIBLE : View.INVISIBLE);
//// viewHolder.tv_gift_change_love_values.setVisibility(View.GONE);
//
//
// //设置礼物名字
// viewHolder.tv_gift_name.setText(giftModel.getGift_name());
// //设置礼物价格
// String surplusTxt = giftModel.getGift_price();
// SpannableStringBuilder stringBuilder = new SpannableStringBuilder(surplusTxt);
// //ForegroundColorSpan 为文字前景色BackgroundColorSpan为文字背景色
// ForegroundColorSpan redSpan = new ForegroundColorSpan(mContext.getResources().getColor(R.color.color_FFA9A9A9));
// stringBuilder.setSpan(redSpan, surplusTxt.length(), surplusTxt.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);//修改最后两个字体的颜色
// viewHolder.tv_gift_price.setText(stringBuilder);
//// viewHolder.item_layout.setTag(R.id.id_gift_tag, giftModel);
//
// //加载礼物图片
// ImageUtils.loadImageView(giftModel.getBase_image(), viewHolder.iv_gift_pic);
// //设置选中后的样式
//
// if (giftModel.isChecked()) {//被选中
// viewHolder.cl_iv_down_on.setBackgroundResource(R.mipmap.room_gift_bjx);
// viewHolder.ivDownOn.setVisibility(View.GONE);
// } else {
// viewHolder.ivDownOn.setVisibility(View.GONE);
// viewHolder.cl_iv_down_on.setBackgroundResource(0);
// }
// //设置
//// //设置礼物心动值
//// if (giftModel.getCardiac().equals("0")) {
//// viewHolder.tv_gift_change_love_values.setBackgroundResource(R.mipmap.room_gift_xin_dong_reduce);
//// viewHolder.tv_gift_change_love_values.setText(String.format("%s", giftModel.getCardiac()));
//// } else {
//// viewHolder.tv_gift_change_love_values.setBackgroundResource(R.mipmap.room_gift_xin_dong_add);
//// viewHolder.tv_gift_change_love_values.setText(String.format("+%s", giftModel.getCardiac()));
//// }
//// if (giftModel.isManghe()) {
//// viewHolder.tv_gift_change_love_values.setVisibility(View.GONE);
//// }
// if (giftModel.getGift_bag() == 10) {
//
// viewHolder.item_layout.setBackgroundResource(R.mipmap.gift_tkzj);
// viewHolder.tv_gift_name.setText("");
// viewHolder.tv_gift_name.setBackgroundResource(R.mipmap.gift_name_tkzj);
// } else if (giftModel.getGift_bag() == 11) {
// viewHolder.tv_gift_name.setText("");
// viewHolder.item_layout.setBackgroundResource(R.mipmap.gift_syzc);
// viewHolder.tv_gift_name.setBackgroundResource(R.mipmap.gift_name_syzc);
// } else if (giftModel.getGift_bag() == 12) {
// viewHolder.tv_gift_name.setText("");
// viewHolder.item_layout.setBackgroundResource(R.mipmap.gift_sjzd);
// viewHolder.tv_gift_name.setBackgroundResource(R.mipmap.gift_name_skzd);
// }
// return convertView;
// }
//
//
// static class ViewHolder {
// public ConstraintLayout item_layout;
// public TextView tv_gift_name, tv_gift_price, tv_gift_num;
// public ImageView iv_gift_pic;
// public TextView tv_gift_change_love_values;
// public ImageView ivDownOn;
// public ImageView iv_gift_select;
// public ImageView im_heart;
// public ConstraintLayout cl_iv_down_on;
// }
}

View File

@@ -1,29 +0,0 @@
package com.xscm.moduleutil.adapter;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.bean.CircleListBean;
import com.xscm.moduleutil.bean.room.RoomOnlineBean;
import com.xscm.moduleutil.utils.ImageUtils;
/**
*@author qx
*@data 2025/6/10
*@description: 显示用户小头像
*/
public class LikeUser2Adapter<T> extends BaseQuickAdapter<T, BaseViewHolder> {
public LikeUser2Adapter() {
super(R.layout.item_like_user);
}
@Override
protected void convert(BaseViewHolder helper,T item) {
if (item instanceof CircleListBean.LikeList) {
ImageUtils.loadHeadCC(((CircleListBean.LikeList) item).getAvatar(), helper.getView(R.id.user_icon));
} else {
// 可扩展:通过接口回调获取头像 URL
ImageUtils.loadHeadCC(((RoomOnlineBean) item).getAvatar(), helper.getView(R.id.user_icon));
}
}
}

View File

@@ -1,74 +0,0 @@
package com.xscm.moduleutil.base
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.voice.lib_base.ext.inflateBindingWithGeneric
import com.xscm.moduleutil.R
open class BaseBottomFragmentDialog<B : ViewBinding?>(private val resourceID: Int) :
DialogFragment() {
var mDatabind: B? = null
val mBinding: B get() = mDatabind!!
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireActivity(), R.style.myChooseDialog)
mDatabind = DataBindingUtil.inflate(LayoutInflater.from(requireContext()), resourceID, null, false) as B
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(mBinding!!.root)
val window = dialog.window
val params = window!!.attributes
params.width = WindowManager.LayoutParams.MATCH_PARENT
params.height = WindowManager.LayoutParams.WRAP_CONTENT
params.gravity = Gravity.BOTTOM
window.attributes = params
dialog.setCanceledOnTouchOutside(true)
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mDatabind = inflateBindingWithGeneric(inflater, container, false)
// return if (mBinding != null) mBinding!!.root else mDatabind?.root
return mBinding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(requireDialog().window!!, false)
requireDialog().setOnShowListener { dialog: DialogInterface? ->
(view.parent as ViewGroup).setBackgroundColor(
Color.TRANSPARENT
)
}
}
override fun onDestroyView() {
super.onDestroyView()
mDatabind = null
}
fun setBundleArgs(bundleArgs: Bundle?): BaseBottomFragmentDialog<B> {
arguments = bundleArgs
return this
}
}

View File

@@ -1,68 +0,0 @@
package com.xscm.moduleutil.base
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.voice.lib_base.ext.inflateBindingWithGeneric
import com.xscm.moduleutil.R
open class BaseFragmentDialog<B : ViewBinding?>(private val resourceID: Int) : DialogFragment() {
var mDatabind: B? = null
val mBinding: B get() = mDatabind!!
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireActivity(), R.style.myChooseDialog)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
mDatabind = DataBindingUtil.inflate<ViewDataBinding>(LayoutInflater.from(requireContext()), resourceID, null, false) as B
dialog.setContentView(mDatabind!!.root)
val window = dialog.window
val params = window!!.attributes
params.width = WindowManager.LayoutParams.MATCH_PARENT
params.height = WindowManager.LayoutParams.WRAP_CONTENT
window.attributes = params
dialog.setCanceledOnTouchOutside(true)
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mDatabind = inflateBindingWithGeneric(inflater, container, false)
return if (mBinding != null) mBinding!!.root else null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(requireDialog().window!!, false)
requireDialog().setOnShowListener { dialog: DialogInterface? ->
(view.parent as ViewGroup).setBackgroundColor(
Color.TRANSPARENT
)
}
}
override fun onDestroyView() {
super.onDestroyView()
mDatabind = null
}
fun setBundleArgs(bundleArgs: Bundle?): BaseFragmentDialog<B> {
arguments = bundleArgs
return this
}
}

View File

@@ -1,66 +0,0 @@
package com.xscm.moduleutil.base;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.ViewDataBinding;
import com.xscm.moduleutil.bean.room.RoomInfoResp;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.List;
public abstract class BaseRoomFragment<P extends BaseRoomPresenter, VDB extends ViewDataBinding> extends BaseMvpFragment<P, VDB> implements BaseRoomContacts.View {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onDestroyView() {
unRegisterWheatViews();
super.onDestroyView();
}
@Override
protected void initView() {
registerWheatViews();
}
/**
* 房间信息
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void roomInfo(RoomInfoResp resp) {
roomInfoUpdate(resp);
}
public abstract void roomInfoUpdate(RoomInfoResp resp);
public abstract void registerWheatViews();
public abstract void unRegisterWheatViews();
public abstract int[] collectCurrentCardiacValues();
protected void tzblChanged() {
}
private int getAmativenessWheatMaoziLevel(int value) {
if (value >= 52000) {
return 3;
} else if (value >= 10000) {
return 2;
} else if (value >= 1000) {
return 1;
} else {
return 0;
}
}
}

View File

@@ -1,65 +0,0 @@
package com.xscm.moduleutil.base
import androidx.lifecycle.*
import com.xscm.moduleutil.http.RetrofitClient
import com.xscm.moduleutil.widget.room.PassRoomException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
/**
*
*/
open class BaseViewModel : ViewModel(), LifecycleObserver {
private var clickTime: Long = 0
var baseRepository = RetrofitClient.getInstance()
private val passRoom by lazy { MutableLiveData<Exception>() }
private val error by lazy { MutableLiveData<Exception>() }
private val finally by lazy { MutableLiveData<Int>() }
//进入房间
var imgListData: MutableLiveData<List<String>> = MutableLiveData()
var addImgData = MutableLiveData<Any>()
//运行在UI线程的协程
fun launchUI(block: suspend CoroutineScope.() -> Unit) = viewModelScope.launch {
try {
block()
} catch (e: Exception) {
if (e is PassRoomException) {
passRoom.value = e
} else {
error.value = e
// throw e
}
} finally {
finally.value = 200
}
}
/**
* 请求失败,出现异常
*/
fun getError(): LiveData<Exception> {
return error
}
/**
* 请求完成,在此处做一些关闭操作
*/
fun getFinally(): LiveData<Int> {
return finally
}
}

View File

@@ -1,34 +0,0 @@
package com.xscm.moduleutil.base
import com.xscm.moduleutil.utils.config.EnvironmentEnum
object ExternalResConstants {
//================================================================================MQTT======================================================================================
// var IS_MQTT_RELEASE = true
// val MQTT_PATH_DEBUG = "tcp://1.13.181.248"
// val MQTT_PATH_RELEASE = "tcp://1.13.101.98"
//
// fun MQTT_PATH(): String {
// return if (IS_MQTT_RELEASE) {
// MQTT_PATH_RELEASE
// } else {
// MQTT_PATH_DEBUG
// }
// }
//================================================================================HTTP======================================================================================
var IS_HTTP_RELEASE: Int = 1 //0 测试环境 1 正式环境 2 辅助环境
val HTTP_PATH_DEBUG: EnvironmentEnum = EnvironmentEnum.TEST
val HTTP_PATH_RELEASE: EnvironmentEnum = EnvironmentEnum.PRODUCTION
val HTTP_AUXILIARY: EnvironmentEnum = EnvironmentEnum.Auxiliary
fun HTTP_PATH(): EnvironmentEnum {
return if (IS_HTTP_RELEASE == 1) {
HTTP_PATH_RELEASE
} else if (IS_HTTP_RELEASE == 3) {
HTTP_AUXILIARY
} else {
HTTP_PATH_DEBUG
}
}
//================================================================================END======================================================================================
}

View File

@@ -1,10 +0,0 @@
package com.xscm.moduleutil.base;
public class RealTimeMessages<T> {
public T data;
public int code;
}

View File

@@ -1,6 +0,0 @@
package com.xscm.moduleutil.base
data class SocketBean(
val code: Int, val data: Any
)

View File

@@ -1,37 +0,0 @@
package com.xscm.moduleutil.base
/**
* Created by xscm on 2020/7/23.
* 描述web url 常量
*/
object WebUrlConstants {
val BASE_URL = CommonAppContext.getInstance().currentEnvironment.h5Url
/**青少年模式*/
val WEB_ADOLESCENT_URL = BASE_URL + "/web/index.html#/pages/feedback/teenage?id=%s"
val WEB_SET_GROUP_URL = BASE_URL + "/web/index.html#/pages/union/setGroup?id=%s&guildId=%s"
/**举报*/
val WEB_REPORT_URL =
BASE_URL + "/web/index.html#/pages/feedback/report?id=%s&fromType=%d&fromId=%s"
/**规则*/
val WEB_RULES_URL = BASE_URL + "/web/index.html#/pages/other/taskDesc"
/**道具商城*/
val WEB_PROP_MALL_URL = BASE_URL + "/web/index.html#/pages/prop/propMall?id=%s"
/**公会*/
val WEB_GUILD_URL = BASE_URL + "/web/index.html#/pages/union/index?id=%s&guild_id=%s"
/**等级*/
val WEB_GRADE_URL = BASE_URL + "/web/index.html#/pages/other/grade?id=%s"
/**反馈*/
val WEB_HELP_URL = BASE_URL + "/web/index.html#/pages/feedback/help?id=%s"
/**邀请*/
val WEB_INVITATION_URL = BASE_URL + "/web/index.html#/pages/other/income?id=%s"
}

View File

@@ -1,11 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2026/1/14 18:47
* 用途app配置的客服用户id和name
*/
class AppCustomerBean {
var user_id: String = ""
var user_name: String = ""
}

View File

@@ -1,9 +0,0 @@
package com.xscm.moduleutil.bean
import java.io.Serializable
class BeforeJoinRoomCheckBean :Serializable {
var room_id:String? = null
var msg:String? = null
var code:Int = -1
}

View File

@@ -1,28 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2026/1/7 19:47
* 用途:
*/
class BlackRoomBean {
var id: Int = 0
var p_room_id: Int = 0
var room_id: Int = 0
var user_id: Int = 0
var meet_user_id: Int = 0
var end_time: Int = 0
var createtime: Int = 0
var status: Int = 0
var heart_value: String? = ""
/* "id": 1,
"p_room_id": 6065,
"room_id": 6071,
"user_id": 20142,
"meet_user_id": 20137,
"end_time": 1767957473,
"createtime": 1767784373,
"status": 1,
"heart_value": null*/
}

View File

@@ -1,20 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/12/21 11:37
* 用途:盲盒转盘状态
*/
class BlindBoxStatus {
var gift_bag_id: Int = 0
var name: String = ""
var status: Int = 0 //0关闭 1开启
var status_str: String = ""
var icon: String = ""
// "gift_bag_id": 11,
// "name": "岁月之城",
// "status": 1,
// "status_str": "开启中",
// "icon": null
}

View File

@@ -1,53 +0,0 @@
package com.xscm.moduleutil.bean;
import java.util.List;
import lombok.Data;
/**
* @author qx
* @data 2025/6/3
* @description: 这是广场中的列表数据
*/
@Data
public class CircleListBean {
public int id;//语圈ID
public int user_id;//用户ID
public String nickname;//用户昵称
public String avatar;//用户头像
public int is_like;//我是否点赞0没有1点赞
public int sex;//性别 1男2女
public String content;//内容
public String like_num;//点赞数
public String rewards_num; //打赏金额
public String is_room;//作者是否在房间中1在0不在
public String room_id;//作者所在房间ID is_room =0 此值小于0
public String comment_num;//评论数
public int is_recommend;//1非推荐2推荐
public String ip;//活跃地址
public String images;////图片 JSON字符串 封面获取第一张
public String createtime;//时间
public String topic_id;
public String share_url;
public List<HeatedBean> title;//话题列表
public String nobility_image;//贵族图标
public String nickname_color = "";//昵称颜色
public String mic_cycle;//麦圈
public String read_num;//阅读数
public List<LikeList> like_list;
@Data
public class LikeList {
public String user_id;
public String nickname;
public String avatar;
public int age;//年龄
public String sex;
public String constellation;//星座
public String birthday;//生日
}
}

View File

@@ -1,174 +0,0 @@
package com.xscm.moduleutil.bean
import com.google.gson.annotations.SerializedName
import java.util.ArrayList
/**
* 项目名称:羽声语音
* 时间2026/1/4 10:22
* 用途:装扮价格详情
*/
class DecorateDetailBean {
// 用户信息服务端返回的user_info字段
@SerializedName("user_info")
var userInfo: UserInfoDecorate? = UserInfoDecorate()
// 装饰商品核心信息服务端返回的decorate字段
@SerializedName("decorate")
var decorate: Decorate? = null
/**
* 用户信息内部类对应服务端user_info
*/
class UserInfoDecorate {
@SerializedName("user_id")
var userId: Int = 0
@SerializedName("user_coin")
var userCoin: String = ""
}
/**
* 装饰商品核心信息对应服务端decorate直接解析服务端返回数据
* 注意与之前适配器的Item模型解耦该类仅负责接收服务端数据不承担适配器布局类型职责
*/
class Decorate {
@SerializedName("title")
var title: String = "" // 商品名称(如“粉色花头”)
@SerializedName("price")
var price : String =""
@SerializedName("base_image")
var base_image : String =""
@SerializedName("price_list")
var priceList: List<PriceListBean> = ArrayList() // 价格/时长列表(服务端返回数组)
}
/**
* 价格/时长明细bean对应服务端price_list中的单个对象
* 可直接解析服务端返回的每个价格项数据同时适配适配器的PriceItem模型
*/
class PriceListBean {
@SerializedName("price")
var price: String = "" // 现价
@SerializedName("original_price")
var originalPrice: String = "" // 原价
@SerializedName("discount")
var discount: String = "" // 折扣如“5.0”)
@SerializedName("day")
var day: Int = 0 // 有效天数
@SerializedName("month")
var month: String = "" // 有效月数
@SerializedName("end_time")
var endTime: String = "" // 有效期截止时间
}
// ---------------------- 适配器适配相关:转换方法 + 适配器所需模型 ----------------------
/**
* 适配器的Item数据模型密封类区分单行/多选项)
* 与服务端数据模型解耦专门用于RecyclerView适配器布局
*/
sealed class DecorateAdapterItem {
// 单行信息类型(如“商品价格”“有效期至”)
data class SingleItem(
val label: String, // 左侧标签文字
val content: String // 右侧内容文字
) : DecorateAdapterItem()
// 购买时长多选项类型(承载所有时长选项)
data class MultiOptionItem(
val options: List<PriceListBean> // 直接复用PriceListBean已适配服务端数据
) : DecorateAdapterItem()
// type=12专用购买次数加减按钮
data class BuyCountItem(
val initialCount: Int,
val unitPrice: String
) : DecorateAdapterItem()
}
/**
* 转换方法将服务端数据Decorate转换为适配器所需数据列表List<DecorateAdapterItem>
* 实现服务端数据与适配器的桥接,方便适配器直接使用
* @param defaultSelectedPos 默认选中的时长选项下标默认0即第一个选项
*/
fun convertToAdapterData(
decorate: Decorate?,
defaultSelectedPos: Int = 0
): List<DecorateAdapterItem> {
val adapterDataList = mutableListOf<DecorateAdapterItem>()
if (decorate == null ) {
return adapterDataList
}
if ( decorate.priceList.isEmpty()){
// ---------- type=12解析单个字段无price_list新增购买次数、商品总价 ----------
val unitPrice = decorate.price// 直接取Decorate的singlePrice服务端返回的单价
val unitPriceStr = unitPrice // 格式化单价,避免小数异常
// 2. 商品单价单行项取decorate.singlePrice
adapterDataList.add(
DecorateAdapterItem.SingleItem(
label = "商品单价",
content = unitPriceStr
)
)
// 3. 购买次数type=12专用初始数量1传入单价用于计算总价
adapterDataList.add(
DecorateAdapterItem.BuyCountItem(
initialCount = 1,
unitPrice = unitPrice
)
)
// 4. 商品总价单行项初始单价×1
adapterDataList.add(
DecorateAdapterItem.SingleItem(
label = "商品总价",
content = unitPriceStr // 初始总价=单价×1
)
)
}else {
// 安全获取默认选中项(防止下标越界)
val selectedPos = if (defaultSelectedPos in decorate.priceList.indices) {
defaultSelectedPos
} else {
0
}
val selectedPriceItem = decorate.priceList[selectedPos]
// 1. 添加“商品价格”单行项(取选中项的现价)
adapterDataList.add(
DecorateAdapterItem.SingleItem(
label = "商品价格",
content = "${selectedPriceItem.price}" // 拼接货币符号,优化展示
)
)
// 2. 添加“有效期至”单行项(取选中项的截止时间)
adapterDataList.add(
DecorateAdapterItem.SingleItem(
label = "有效期至",
content = selectedPriceItem.endTime
)
)
// 3. 添加“购买时长”多选项(承载所有价格/时长列表)
adapterDataList.add(
DecorateAdapterItem.MultiOptionItem(
options = decorate.priceList
)
)
}
return adapterDataList
}
}

View File

@@ -1,34 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/11/27 16:47
* 用途:
*/
class FamilyEarnings {
var id: Int=0
var nickname: String=""
var avatar: String=""
var user_code: String=""
var earnings: String=""
var createtime: Long=0
var gift_name: String=""
var gift_price: String=""
/* id 列表 id
nickname 用户昵称
avatar
user_code
earnings 收益
createtime
gift_name 礼物名称
gift_price 礼物价格*/
}

View File

@@ -1,11 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/12/30 9:44
* 用途:节日主题接口
*/
class FestivalThemeBean {
var is_open : Int = 0 //主题开关
var theme_name : String = "" //主题名称
}

View File

@@ -1,117 +0,0 @@
package com.xscm.moduleutil.bean;
import java.util.List;
import lombok.Data;
/**
* @author qx
* @data 2025/5/27
* @description: 礼盒数据
*/
@Data
public class GiftBoxBean {
private String user_gold;//累计获取的金币
private List<GiftBean> gift_box_list;
private List<TaskDataBean> tasks;
@Data
public static class GiftBean {
// private String giftName; //初级礼盒、高级礼盒
// private String giftTitle; //最高可以获得的金币数
// private String getGiftTypeName; //满多少个金币
// private String giftTypeNumber; //当前的百分比
// private String giftTypeStatus; //是否已经可以
/*{
"id":2,
"name":"高级礼盒",
"title":"最高可获得10000金币",
"icon":"",
"highest_gain":"10000",
"meet":"300",
"unlock_progress":0,
"all_number":"1",
"taday_number":0,
"taday_number_left":1,
"status":0,
"status_str":"未解锁"
}*/
private String id;//礼盒ID
private String name;//礼盒名称
private String title; //标题
private String icon;//图标
private String highest_gain;//最高获得金币数
private String meet;//满多少金币可抽
private String unlock_progress;// //解锁进度
private String all_number;// //今日可抽奖次数
private String taday_number;////今日剩余抽奖次数
private String taday_number_left;// //今日剩余抽奖次数
private String status;////状态:0 '未解锁 1已解锁 2抽奖次数已用完
private String status_str;//"已解锁(0/2)"
}
@Data
public static class TaskDataBean {
private List<DailyTasksBean> task_list;
// private List<DailyTasksBean> daily_tasks_special;
// private List<DailyTasksBean> usual_tasks;
// private List<DailyTasksBean> teacher_tasks;
private int task_type_id;
private String task_type_name;
private int is_lock;//锁:0 不开启锁 1 开启锁
private int wait_reward_num;//待领取奖励数量
@Data
public static class DailyTasksBean {
/* {
"task_id":20,
"icon":null,
"task_name":"每日在房间时长 15 分钟(0/15)",
"target_quantity":900,
"task_type":2,
"jump_type":4,
"tasks_bag_id":0,
"task_status":1,
"task_type_str":"去完成",
"jump_type_str":"跳转房间",
"from_id":6040,
"is_time":1,
"processing_type":2,
"processing_type_str":"去观看",
"reward_str":"25金币笨笨狗x1,
仙女之星头像框,
三八大杠"
}*/
private int task_id;////任务Id
private String icon;//图标
private String task_name;//任务名称
private int target_quantity;//目标完成数量
private int task_type;//任务类型 1每日任务 2每日特殊任务 3平台常规任务
/**
* 0 不跳转
* 1 跳转实名
* 2 跳转我的相册
* 3 跳转绑定管理
* 4 跳转房间
* 5 跳转申请加入公会
* 6 跳转充值
*/
private int jump_type; //跳转类型
private int tasks_bag_id; //任务礼盒ID
private int task_status;//任务状态1完成 2去领取 3已领取
private String task_type_str; //任务状态
private String jump_type_str; //跳转类型提示
private String from_id;
private int is_time;
private int processing_type;//跳转状态:
private String processing_type_str;//跳转状态
private String reward_str;//1奖励
private String student_id;//徒弟ID
}
}
}

View File

@@ -1,22 +0,0 @@
package com.xscm.moduleutil.bean
import java.util.ArrayList
/**
* 项目名称:羽声语音
* 时间2026/1/23 9:59
* 用途:礼物墙用户列表
*/
class GiftWallUserBean {
var count : Int = 0
var users :List<GiftWallUserItemBean> = ArrayList()
class GiftWallUserItemBean {
var avatar : String = ""
var nickname : String = ""
var user_id : String = ""
var count : Int = 0
}
}

View File

@@ -1,54 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2026/1/3 10:13
* 用途:群聊实体类
*/
class GroupBean {
var guild_id: String = ""
var guild_cover: String = ""
var is_deacon: Int = 0 //是否是群主 1是 ,其他的不是 2:管理员 3普通成员
var user_list: List<GroupUserBean> = ArrayList()
var name: String = ""
var notification: String = ""
var mute_all_member : Int = 0 //是否全体禁言 1是 0不是
class GroupUserBean {
var is_online: Int = 0 //是否在线 1在线 0不在线
var market_value: Int = 0 //身价
var nickname: String = ""
var avatar: String = ""
var user_code: String = ""
var user_id: Int = 0
var createtime: String = ""
var is_self: Int = 0 //是否是本人 1是 0不是
var role: String = ""
var role_str: String = ""
var in_room_id: Int = 0 //是否在房间内 1在 0不在
var is_mute: Int = 0 //是否被禁言 1是 0不是
var group_role : Int = 0 //群角色 1群主 2管理员 3普通成员
}
/*"guild_id": "f627",
"guild_cover": "https://yusheng-1369267578.cos.ap-guangzhou.myqcloud.com/images/android_images/325ee1f528343bb09ddc086b2b83b190.jpg",
"is_deacon": 1,
"user_list": [
{
"is_online": 1,
"market_value": 28,
"nickname": "🥭芒的很",
"avatar": "https://yusheng-1369267578.cos.ap-guangzhou.myqcloud.com/images/ios_images/1764941796523.jpeg",
"user_code": "10001",
"user_id": 21211,
"createtime": "2025-12-31 13:20:28",
"is_self": 0,
"role": "Member",
"role_str": "普通群成员",
"in_room_id": 0
}
],
"name": "美丽的眼神的家族",
"notification": ""*/
}

View File

@@ -1,13 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2026/1/3 14:07
* 用途:群成员列表
*/
class GroupUserListBean {
var page : String=""
var limit : String=""
var count : String=""
var list : List<GroupBean.GroupUserBean>?= ArrayList()
}

View File

@@ -1,102 +0,0 @@
package com.xscm.moduleutil.bean
import java.io.Serializable
class HeartCpBean : Serializable {
/* {
"code": 1,
"msg": "\u6210\u529f",
"data": {
"id": 1,
"user_id1": 20001,
"user_id2": 20003,
"status": 1,
"level": 1,
"exp": 9198,
"createtime": 1763720783,
"user_info1": {
"user_id": 20001,
"nickname": "\u9ad8\u5174\u7684\u5c0f\u6d77\u817e",
"avatar": "https:\/\/yusheng-1369267578.cos.ap-guangzhou.myqcloud.com\/images\/ios_images\/1761615690733.jpeg"
},
"user_info2": {
"user_id": 20003,
"nickname": "\u79c0\u4e3d\u7684\u978b\u57ab",
"avatar": "https:\/\/yusheng-1369267578.cos.ap-guangzhou.myqcloud.com\/images\/android_images\/33e31b18577856c4f10906b7cabaa698.jpg"
},
"next_level_exp": 802,
"pendant": "https:\/\/cos.xscmmidi.site\/admin\/520liuxingyu_17637096968287.mp4",
"gift_log": [
{
"id": 323,
"room_id": 6003,
"from_user_id": 20001,
"to_user_id": 20003,
"gift_id": 313,
"num": 1,
"cp_zone_id": 1,
"exp": "1314",
"exp_total": "9198",
"remark": "\u9001\u7ed9\u79c0\u4e3d\u7684\u978b\u57ab1\u4e2a\u9e4a\u7f18\u7ec7\u68a6,\u83b7\u5f971314\u7ecf\u9a8c\u503c\uff0c\u603b\u7ecf\u9a8c\u503c\u589e\u52a0\u81f39198",
"createtime": 1763796118,
"gift_name": "\u9e4a\u7f18\u7ec7\u68a6",
"from_user_info": {
"user_id": 20001,
"nickname": "\u9ad8\u5174\u7684\u5c0f\u6d77\u817e",
"avatar": "https:\/\/yusheng-1369267578.cos.ap-guangzhou.myqcloud.com\/images\/ios_images\/1761615690733.jpeg"
},
"to_user_info": {
"user_id": 20003,
"nickname": "\u79c0\u4e3d\u7684\u978b\u57ab",
"avatar": "https:\/\/yusheng-1369267578.cos.ap-guangzhou.myqcloud.com\/images\/android_images\/33e31b18577856c4f10906b7cabaa698.jpg"
}
}]
},
"api_version": ""
}*/
var id: Int = 0
var user_id1: Int = 0
var user_id2: Int = 0
var status: Int = 0
var level: Int = 0
var exp: Long = 0
var createtime: Long = 0
var user_info1: UserInfo = UserInfo()
var user_info2: UserInfo = UserInfo()
var next_level_exp: Long = 0
var pendant: String = ""
var gift_log: List<GiftLog> = ArrayList()
var api_version: String = ""
class GiftLog : Serializable {
var id: Int = 0
var room_id: Int = 0
var from_user_id: Int = 0
var to_user_id: Int = 0
var gift_id: Int = 0
var num: Int = 0
var cp_zone_id: Int = 0
var exp: String = ""
var exp_total: String = ""
var remark: String = ""
var createtime: Long = 0
var gift_name: String = ""
var from_user_info: UserInfo = UserInfo()
var to_user_info: UserInfo = UserInfo()
class UserInfo : Serializable {
var user_id: Int = 0
var nickname: String = ""
var avatar: String = ""
}
}
class UserInfo : Serializable {
var user_id: Int = 0
var nickname: String = ""
var avatar: String = ""
}
}

View File

@@ -1,18 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/12/8 16:30
* 用途:首页弹框推荐房间
*/
class IndexRecommendRoom {
var room_id: String? = ""
var user_id: String? =""
var room_name: String? = ""
var room_cover: String? = ""
/* "id": "string",
"user_id": "string",
"room_name": "string",
"room_cover": "string"*/
}

View File

@@ -1,15 +0,0 @@
package com.xscm.moduleutil.bean;
import com.xscm.moduleutil.BaseEvent;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class MqttXlhEnd extends BaseEvent implements Serializable {
private static final long serialVersionUID = 1L;
private String message;
}

View File

@@ -1,95 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/11/26 18:45
* 用途:
*/
class MyFamilyBean {
var group_id: String = "" //群组id
var name: String = "" //家族名称
var user_id: Int = 0 //族长id
var group_members_num: Int = 0 //签约次数 群组人数
var group_earnings: String = "" //总计收益
var group_owner_info: GroupOwnerInfoBean? = null //族长信息
class GroupOwnerInfoBean {
var nickname: String = "" //昵称
var avatar: String = "" //头像
var dress: String = "" //头像框
var user_code: String = "" //用户id
var icon: List<String> = arrayListOf()
var mic_cycle: String = "" //麦圈
var nobility_image: String = "" //贵族
var nickname_color: String = "" //贵族颜色
}
var group_members_lists: List<GroupMembersListsBean> = arrayListOf() //
class GroupMembersListsBean{
var market_value: String = "" //身价
var nickname: String = ""
var avatar: String = ""
var user_code: String = ""
var user_id: Int = 0
var end_time: Long = 0 //签约到期时间点
var dress: String = ""
var icon: List<String> = arrayListOf()
var mic_cycle: String = "" //麦圈
var nobility_image: String = "" //贵族
var nickname_color: String = "" //贵族颜色
var free_renewal: Int = 0 //免费续签次数
var today_earnings: Double = 0.0 //今日收礼收益
var yesterday_earnings: Double = 0.0 //昨日收礼收益
var is_online: Int = 0 //是否在线 1在线 0离线
var is_show_sign: Int = 0 //是否显示续约按钮 1显示 0不显示
var sign_times: Int = 0 //非首签签约时长
var sign_user_ratio : Int = 0 //非首签签约比例
var end_day: String = "" //签约剩余时长
val free_re_sign_day : Int=0 //免费续签时长
}
/* "group_id": "string",
"name": "string",
"user_id": "string",
"group_members_num": "string",
"group_earnings": "string",
"group_owner_info": {
"nickname": "string",
"avatar": "string",
"dress": "string",
"user_code": "string",
"icon": [
"string"
],
"mic_cycle": "string",
"nobility_image": "string",
"nickname_color": "string"
},
"group_members_lists": [
{
"market_value": "string",
"nickname": "string",
"avatar": "string",
"user_code": "string",
"user_id": "string",
"end_time": "string",
"dress": "string",
"icon": [
"string"
],
"mic_cycle": "string",
"nobility_image": "string",
"nickname_color": "string",
"free_renewal": "string",
"today_earnings": "string",
"yesterday_earnings": "string"
}
]
}*/
}

View File

@@ -1,82 +0,0 @@
package com.xscm.moduleutil.bean
/**
*com.xscm.moduleutil.bean
*qx
*2025/11/8
*
*/
class NobilitDeatils {
var nobility_power_list = ArrayList<nobilityPowerItem>()
var user_info: UserInfo? = null
var nobility_info: NobilityInfo? = null
class UserInfo {
/*"user_info": { // 用户信息
"id": 20034, //用户Id
"nickname": "奋斗的石头", //用户昵称
"avatar": "http://test.vespa.qxyushen.top/data/avatar/head_pic.png" //用户头像
}*/
var id: Int = 0
var nickname: String = ""
var avatar: String = ""
}
class NobilityInfo {
/*"nobility_info": { // 爵位信息
"status": 0, //状态: 0去开通 1去续费
"lid": 0, //爵位ID
"name": "", // 爵位名称
"image": "", //爵位图片
"end_time": 0 //结束时间
}*/
var status: Int = 0
var name: String = ""
var lid: Int = 0
var image: String = ""
var end_time: String = ""
}
class nobilityPowerItem {
/*"lid": 0, //爵位ID
"name": "特权", //爵位名称
"power_ids": "", //权限ID
"nick_name_color": "无", //昵称颜色
"nick_name_color_name": "无", //昵称颜色名称
"nobility_list": [
{
"id": 1, //权限ID
"name": "专属徽章", //权限名称
"image": "", //权限图片
"status": 0 //权限状态 0 不显示 1显示
},
{
"id": 2,
"name": "昵称颜色",
"image": "",
"status": 0
}
]*/
var lid: Int = 0
var name: String = ""
var power_ids: String = ""
var nick_name_color: String = ""
var nick_name_color_name: String = ""
var nobility_list = ArrayList<nobilityPowerItem>()
class nobilityPowerItem {
/*"id": 1, //权限ID
"name": "专属徽章", //权限名称
"image": "", //权限图片
"status": 0 //权限状态 0 不显示 1显示*/
var id: Int = 0
var name: String = ""
var image: String = ""
var status: Int = 0
}
}
}

View File

@@ -1,51 +0,0 @@
package com.xscm.moduleutil.bean
/**
*com.xscm.moduleutil.bean
*qx
*2025/11/10
*
*/
class NobilitList {
var name: String = ""
var image: String = ""
var pay_price: String = ""
var power: Power? = Power()
var lid: Int = 0
var day : Int = 0
class Power {
var power_count: Int = 0
var this_power_count: Int = 0
var count_str: String = ""
var list = ArrayList<PowerItem>()
class PowerItem {
var id: Int = 0
var name: String = ""
var content: String = ""
var image: String = ""
}
}
/* {
"name": "骑士",
"image": "https://cos.xscmmidi.site/admin/ScreenShot_2025-11-05_175044_144_17623369241246.png",
"pay_price": "388.00",
"power": {
"power_count": 12,
"this_power_count": 1,
"count_str": "12/1",
"list": [
{
"id": 1,
"name": "专属徽章",
"content": "专属动态爵位徽章",
"image": ""
}
]
}
},
*/
}

View File

@@ -1,49 +0,0 @@
package com.xscm.moduleutil.bean
/**
*com.xscm.moduleutil.bean
*qx
*2025/11/10
*
*/
class NobilityPrice {
var lid : Int = 0
var nobility_name : String = ""
var nobility_image : String = ""
var price : String = ""
var pay_price : String = ""
var day : Int = 0
var end_time : String = ""
var power_list = ArrayList<PowerItem>()
class PowerItem {
var id : Int = 0
var content : String = ""
}
/* "lid": 2,
"nobility_name": "男爵",
"nobility_image": "https://cos.xscmmidi.site/admin/ScreenShot_2025-11-05_175121_076_17623369074684.png",
"price": "588.00", //实付价格
"pay_price": "588.00", //画线价格
"day": 30,
"power_list": [
{
"id": 1,
"content": "专属动态爵位徽章"
},
{
"id": 2,
"content": "设置昵称颜色字体"
},
{
"id": 3,
"content": "特殊入场动画音效"
},
{
"id": 4,
"content": "入场专属座驾动画"
}
],
"end_time": "2025-12-10 10:19:20"*/
}

View File

@@ -1,57 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2026/1/3 18:44
* 用途:装扮列表
*/
class PersonaltyListBean {
var did: Int =0 // 装扮id
var title: String = "" // 装扮名称
var type: Int = 0 // 1头像框 2坐骑 3资料展示特效 4光圈 5气泡 6个人靓号 7房间靓号 8工会靓号 100热门
var base_image: String = "" // 展示图片
var play_image: String = "" // 播放图像
var price: Int = 0 // 实际价格(金币)
var special_num: Int = 0 // 靓号
var original_price: Int = 0 // 原价
var discount: Double = 0.0 // 折扣
var discount_str: String = "" // 折扣字段
/* title
装扮名称
type
类型1头像框 2坐骑 3资料展示特效 4光圈 5气泡 6个人靓号 7房间靓号 8工会靓号 100热门
base_image
展示图片
play_image
播放图像
price
实际价格(金币)
special_num
靓号
original_price
原价
discount
折扣
discount_str
折扣字段*/
}

View File

@@ -1,13 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2026/1/6 14:25
* 用途:
*/
class PitTimeRespBean {
var time: Int = 0
var time_str: String = ""
/* "time": 5,
"time_str": "5分钟"*/
}

View File

@@ -1,11 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/12/30 18:02
* 用途:红包配置信息接口
*/
class RedPacketConfig {
var red_packet_min_amount : Int = 0 //发红包最小金额
var red_packet_fee : Int = 0 //发红包手续费
}

View File

@@ -1,73 +0,0 @@
package com.xscm.moduleutil.bean
import com.chad.library.adapter.base.entity.MultiItemEntity
import lombok.Data
import java.util.ArrayList
/**
* 项目名称:羽声语音
* 时间2025/11/20 15:13
* 用途:心动空间中的关系实体
*/
@Data
class RelationBean : MultiItemEntity {
var cp: UserInfo.CpInfo = UserInfo.CpInfo()
var no_cp: List<NoCpBean> = ArrayList()
override fun getItemType(): Int {
// 情况3no_cp集合relation_name不等于""并且relation_list的大小是1
if (no_cp.isNotEmpty() && no_cp[0].relation_name.isNotEmpty() && no_cp[0].relation_list.size == 1) {
return 3
}
// 情况4no_cp集合relation_list的大小是1
if (no_cp.isNotEmpty() && no_cp[0].relation_list.size == 1) {
return 4
}
if (no_cp.isNotEmpty() && no_cp[0].relation_name.isNotEmpty() && no_cp[0].relation_list.size > 1){
return 5
}
// 其他情况返回5
return 6
}
class NoCpBean : MultiItemEntity{
var relation_name: String = ""
var relation_list: List<RelationshipBean> = ArrayList()
override fun getItemType(): Int {
return 0
}
}
/*"cp": [
{
"relation_name": "string",
"relation_list": [
{
"nickname1": "string",
"avatar1": "string",
"user_id1": "string"
}
]
}
],
"no_cp": [
{
"relation_name": "string",
"relation_list": [
{
"user_info1": {},
"user_info2": "string",
"level": "string",
"exp": "string"
}
]
}
]
}*/
}

View File

@@ -1,38 +0,0 @@
package com.xscm.moduleutil.bean;
import com.xscm.moduleutil.BaseEvent;
import org.greenrobot.eventbus.EventBus;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 送礼后的成功回调
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class RoomGiftData extends BaseEvent {
private int gift_total;
private List<GiftUserData> gift_user_data;
private CpType cp_type;
@Data
public static class GiftUserData {
private String user_id;//受理人id
private String gift_price;//礼物价格
}
@Data
public static class CpType {
private int cp_type;//0不处理 1表明心意 2组成cp
private String text;//发送方弹起的信息
private String text1;//接收方弹起的信息
private int gift_id;//礼物id
}
}

View File

@@ -1,115 +0,0 @@
package com.xscm.moduleutil.bean
import com.chad.library.adapter.base.entity.MultiItemEntity
/**
* 项目名称:羽声语音
* 时间2025/12/12 10:35
* 用途:签到实体类
*/
class SignInData : MultiItemEntity{
val TYPE_NORMAL: Int = 0 // 普通类型使用day_sgin布局
val TYPE_DOUBLE: Int = 1 // 双倍宽度类型使用day_sgin2布局
var viewType: Int = TYPE_NORMAL // 默认为普通类型
var day: Int = 0 //第几天
var sign_in_date: String = "" //日期
var sign_in_status: Int = 0 // 签到状态1已签到 0未签到
var sign_in_status_str: String = "" //已签到
var is_today: Int = 0 //是否是当天 0否 1是
var gift_name: String = "" //礼物名称
var num: String = "" //数量
var gift_price: Int = 0 //价格
var type: Int = 0 //类型: 1金币 2礼物 3装扮 4钻石
var base_image: String = "" //礼物图片
override fun getItemType(): Int {
return viewType
}
/* "gift_bag": [
{
"day": 1, //第几天
"sign_in_date": "2025-12-09", //日期
"sign_in_status": 1, //签到状态1已签到 0未签到
"sign_in_status_str": "已签到",
"is_today": 0, //是否是当天 0否 1是
"gift_name": "金币", //礼物名称
"num": 1, //数量
"gift_price": 1, //价格
"type": 1, //类型: 1金币 2礼物 3装扮 4钻石
"base_image": "https://test.vespa.qxyushen.top/static/image/icon/gold.png" //礼物图片
},
{
"day": 2,
"sign_in_date": "2025-12-10",
"sign_in_status": 1,
"sign_in_status_str": "已签到",
"is_today": 0,
"gift_name": "打工狗",
"num": "1 个",
"gift_price": "9",
"type": 2,
"base_image": "https://cos.xscmmidi.site/admin/3143tianxuanbanzhuangou_17615346376493.png"
},
{
"day": 3,
"sign_in_date": "2025-12-11",
"sign_in_status": 1,
"sign_in_status_str": "已签到",
"is_today": 0,
"gift_name": "神皇",
"num": "3 天",
"gift_price": 30,
"type": 3,
"base_image": "https://midi01.oss-cn-beijing.aliyuncs.com/5544shenhuang_17579016149523.png"
},
{
"day": 4,
"sign_in_date": "2025-12-12",
"sign_in_status": 1,
"sign_in_status_str": "已签到",
"is_today": 1,
"gift_name": "心动喷发",
"num": "2 个",
"gift_price": "100",
"type": 2,
"base_image": "https://cos.xscmmidi.site/admin/3300xindongpenfa_17615347008740.png"
},
{
"day": 5,
"sign_in_date": "2025-12-13",
"sign_in_status": 0,
"sign_in_status_str": "未签到",
"is_today": 0,
"gift_name": "钻石",
"num": 20,
"gift_price": 1,
"type": 4,
"base_image": "https://test.vespa.qxyushen.top/static/image/icon/gold.png"
},
{
"day": 6,
"sign_in_date": "2025-12-14",
"sign_in_status": 0,
"sign_in_status_str": "未签到",
"is_today": 0,
"gift_name": "王者之戒",
"num": "1 个",
"gift_price": "19",
"type": 2,
"base_image": "https://cos.xscmmidi.site/admin/5390wangzhezhijie_17615353361010.png"
},
{
"day": 7,
"sign_in_date": "2025-12-15",
"sign_in_status": 0,
"sign_in_status_str": "未签到",
"is_today": 0,
"gift_name": "财源滚滚",
"num": "30 天",
"gift_price": 288,
"type": 3,
"base_image": "https://midi01.oss-cn-beijing.aliyuncs.com/2160caiyuangunguntouxiangkuang_17579008394073.png"
}
]*/
}

View File

@@ -1,21 +0,0 @@
package com.xscm.moduleutil.bean
import java.io.Serializable
class SignInfo : Serializable {
/* sign_id
// 0 未开始 1进行中
sign_status;
// 签约天数
sign_day;
// 当前身价
current_body_value;
// 倒计时 时间戳
end_time;*/
var sign_id: String = ""
var sign_status: Int = 0
var sign_day: Int = 0
var current_body_value: Int = 0
var end_time: Long = 0
}

View File

@@ -1,54 +0,0 @@
package com.xscm.moduleutil.bean;
import java.io.Serializable;
import lombok.Data;
@Data
public class SingerInfo implements Serializable {
private static final long serialVersionUID = 1L;
private SongInfo song_info;
private SongInfo next_song_info;
@Data
public static class SongInfo implements Serializable{
private static final long serialVersionUID = 1L;
/*"id": 29,
"room_id": 6001,
"user_id": 20001,
"singer_song_id": 9,
"status": 1,
"sort": 0,
"createtime": 1763435086,
"boss_user_id": 20001,
"boss_nickname": "高兴的小海腾",
"boss_avatar": "https://yusheng-1369267578.cos.ap-guangzhou.myqcloud.com/images/ios_images/1761615690733.jpeg",
"boss_dress": "",
"boss_mic_cycle": "https://cos.xscmmidi.site/admin/ripple3695_17627709565119.svga",
"singer_user_id": 20000,
"singer_nickname": "坚定的故事",
"singer_avatar": "https://yusheng-1369267578.cos.ap-guangzhou.myqcloud.com/images/android_images/4ead5077435f1da7b8aae1a878bb5ac9.jpg",
"singer_dress": "",
"singer_mic_cycle": "https://cos.xscmmidi.site/admin/ripple3695_17627709565119.svga",
"song_name": "公敌"*/
private int id = 0;
private int room_id = 0;
private int user_id = 0;
private int singer_song_id = 0;
private int status = 0;
private int sort = 0;
private long createtime = 0;
private int boss_user_id = 0;
private String boss_nickname = "";
private String boss_avatar = "";
private String boss_dress = "";
private String boss_mic_cycle = "";
private int singer_user_id = 0;
private String singer_nickname = "";
private String singer_avatar = "";
private String singer_dress = "";
private String singer_mic_cycle = "";
private String song_name = "";
}
}

View File

@@ -1,40 +0,0 @@
package com.xscm.moduleutil.bean
/**
* SingerSongCount 类,用于存储歌手和歌曲数量信息
* 这个类可能用于统计或展示每位歌手的歌曲数量
*/
class SingerSongCount {
// 类定义结束,这里可以添加属性和方法来存储和操作歌手及其歌曲数量
var total: Int = 0
var today: Int = 0
var yesterday: Int = 0
var week: Int = 0
var month: Int = 0
var already: Int = 0
/* total
string
总数
必需
today
string
今天
必需
yesterday
string
昨天
必需
week
string
本周
必需
month
string
本月
必需
already
string
已点*/
}

View File

@@ -1,67 +0,0 @@
package com.xscm.moduleutil.bean
class SongPlaylist {
var count: Int = 0
var lists: List<SongPlaylistBean> = ArrayList()
class SongPlaylistBean {
var id: Int = 0
var room_id :String =""
var user_id: String = ""
var singer_song_id: String = ""
var status:String =""
var sort: Int = 0
var boss_nickname: String = ""
var song_name: String = ""
var gift_id: String = ""
var gift_num: String = ""
var createtime: String = ""
var gift_name: String = ""
var gift_price: String = ""
var base_image: String = ""
var singer_nickname: String = ""
}
/* id
string 列表ID
user_id
string
歌手ID
song_name
string
歌曲名
gift_id
string
礼物ID
gift_num
string
礼物数量
createtime
string
添加时间
gift_name
string
礼物名称
gift_price
string
礼物价格
base_image
string
礼物图片
nickname
string
歌手昵称*/
}

View File

@@ -1,12 +0,0 @@
package com.xscm.moduleutil.bean
/**
*com.xscm.moduleutil.bean
*qx
*2025/11/8
*
*/
class TableCellData {
var title: String = ""
var color: String = ""
}

View File

@@ -1,11 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/12/16 11:35
* 用途:任务未领取角标
*/
class TasksMessage {
var num: Int=0
var not_received_tasks_num : Int=0 //任务未领取奖励数 这是心跳中返回的参数,用于展示任务未领取角标,是在房间内展示的
}

View File

@@ -1,11 +0,0 @@
package com.xscm.moduleutil.bean
/**
* 项目名称:羽声语音
* 时间2025/12/12 14:54
* 用途:签到状态
*/
class TasksSignStatus {
var status : Int=0
var status_str: String=""
}

View File

@@ -1,126 +0,0 @@
package com.xscm.moduleutil.bean;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.base.CommonAppContext;
import lombok.Data;
/**
* @author qx
* @data 2025/8/12
* @description: 主题接口,修改主题返回的值
*/
@Data
public class ThemeBean {
private int is_open;
private String theme_name;
private String theme_color="#22BB79";//主题颜色
private String file_url;//
private String auxiliary_color;//
private String btn_text_color="#3ABC6D";//按钮文字颜色
private int app_bg;//app背景图
private int home_sel;//首页选中
private int home_nor;//首页未选中
private int find_sel;//广场选中
private int find_nor;//广场未选中
private int msg_sel;//消息选中
private int msg_nor;//消息未选中
private int mine_sel;//我的选中
private int mine_nor;//我的未选中
public int getIs_open() {
CommonAppContext.getInstance().is_open = is_open;
return is_open;
}
public void setIs_open(int is_open) {
this.is_open = is_open;
CommonAppContext.getInstance().is_open = is_open;
}
public String getTheme_color() {
return "#22BB79";
}
public String getFile_url() {
return file_url;
}
public String getAuxiliary_color() {
return auxiliary_color;
}
public String getBtn_text_color() {
// if (CommonAppContext.getInstance().is_open == 1){
// return "#FF663B";
// }
return "#FFFFFF";
}
public int getApp_bg() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.bg_dark;
}else {
return R.mipmap.activity_bj;
}
}
public int getHome_sel() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_sy_select;
}
return R.mipmap.tab_main_media_selected;
}
public int getHome_nor() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_sy_notselect;
}
return R.mipmap.tab_main_media_unselected;
}
public int getFind_sel() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_dt_select;
}
return R.mipmap.icon_me_trend_select;
}
public int getFind_nor() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_dt_notselect;
}
return R.mipmap.icon_me_trend_unselect;
}
public int getMsg_sel() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_xx_select;
}
return R.mipmap.icon_news_select;
}
public int getMsg_nor() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_xx_notselect;
}
return R.mipmap.icon_news_un_select;
}
public int getMine_sel() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_wd_select;
}
return R.mipmap.icon_my_select;
}
public int getMine_nor() {
if (CommonAppContext.getInstance().is_open == 1){
return R.mipmap.icon_wd_notselect;
}
return R.mipmap.icon_my_un_select;
}
}

View File

@@ -1,196 +0,0 @@
package com.xscm.moduleutil.bean;
import com.chad.library.adapter.base.entity.MultiItemEntity;
import com.google.gson.annotations.SerializedName;
import com.xscm.moduleutil.BaseEvent;
import com.xscm.moduleutil.bean.room.FriendInfo;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author qx
* @data 2025/6/3
* @description: 个人信息,点击我的获取
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class UserInfo extends BaseEvent implements Serializable {
private static final long serialVersionUID = 1L;
public static final String FEMALE = "2";
public static final String MALE = "1";
private int id;
private int user_id = 0; //用户id
private String user_code;//用户id码
private String avatar;//头像
private String nickname;//昵称
private int sex;//性别 1男 2女
private List<String> icon;//等级图标数组
private int follow_num;//关注数
private int fans_num;//粉丝数
private int look_me_num; //被查看次数需要魅力等级20以上才能查看
private int charm_level; //魅力等级
private int is_use_code; //是否是靓号 0否 1是
private String tencent_im;
private String token;
private String jia_jia;//坐骑
private int is_in_pit;//是否在麦上1在0不在
private int is_open_live_remind;//是否设置开播提醒
private String birthday;//生日
private String profile = "";//简介
private String home_bgimages;//背景图片
private int is_follow;//是否关注
private List<UserTagBean> tag_list;
private List<GiftWall> gift_wall;
private int age;//年龄
private String is_room;
private String dress;//头像框
private String chat_bubble;//聊天气泡
private String charm;//魅力值
private String room_id;
private String guild_name = "";//公会名称
private int guild_id = 0;//公会id
private String me_guild_name = "";//公会名称
private int me_guild_id = 0;//公会id
private String is_mute;//禁言状态 0否 1是
private String is_mute_pit;//禁麦状态 0否 1是
private String is_manager;//是否是管理员 0否 1是
private String is_host;//是否是主持 0否 1是
private String is_room_owner;//是否是房主 0否 1是
private String pit_number;//在点击麦上用户的时候使用
private String auction_id;//在拍卖中的拍卖序号
private int auth;//是否实名 1实名 0未实名
private String red_status;
private String gift_num;
private int is_can_chat;//是否可以私聊1可以0不可以
private int can_chat_money;//需要充值的金额
private RelationshipBean qinmi;
private RelationshipBean zhenai;
private String mobile;//手机号
private int heartId; // "heartId": 4,
private int heartNum; //
private String red_num;
private String ta;
private String nobility_image;//贵族图标
private String nickname_color = "";//昵称颜色
private String mic_cycle;//麦圈
private String is_hide;//0不能设置1可以设置
private String hide_status;//0-取消隐身1-设置隐身
private String enter_image;//爵位飘屏的背景
private String enter_text;//爵位飘屏的文字
private int singer_status;//歌手认证状态0-待审核1-通过2-拒绝 -1未认证
private int singer_level;//歌手等级
private CpInfo cp_info;
private int market_value;//身价
private String market_value_coin="";//身价配置 多少金币数
private String market_value_market="";//身价配置 多少身价数
private String sign_value;
private String sign_id;
private Master master;
private int is_online;//是否在线 : 1在线 2离线
private int had_custom_gift;//是否显示设置了自定义礼物 0没有 1
@Data
public static class Master implements Serializable {
private String user_id;
private String nickname;
private String avatar;
}
/*"cp_info": {
"name": "string",
"user_id1": "string",
"user_id2": "string",
"level": "string",
"exp": "string",
"pendant": "string",
"direction": "string",
}*/
@Data
public static class CpInfo implements Serializable, MultiItemEntity {
private int id;
private int user_id1;
private int user_id2;
private int status;
private long createtime;
public String name = "-1";
public String level = "-1";
public String exp = "-1";
public String pendant = "-1";
public String direction = "-1";
public UserInfo1 user_info1;
public UserInfo2 user_info2;
@Override
public int getItemType() {
if (name != null && !name.isEmpty()) {
return 1;
}
return 2;
}
@Data
public static class UserInfo1 implements Serializable {
public String user_id;
public String nickname;
public String avatar;
public String dress;
}
@Data
public static class UserInfo2 implements Serializable {
public String user_id;
public String nickname;
public String avatar;
public String dress;
}
/*"user_info1": {
"user_id": "string",
"nickname": "string",
"avatar": "string",
"dress": "string"
},
"user_info2": {
"user_id": "string",
"nickname": "string",
"avatar": "string",
"dress": "string"
}*/
}
// @Data
// public static class TagList{
// private String id;
// private String tag_name;
// }
@Data
public static class GiftWall implements Serializable {
private String gift_name;
private String total;
private List<SendUserInfo> send_user_info;
@Data
public static class SendUserInfo implements Serializable {
private String nickname;
private String avatar;
}
}
}

View File

@@ -1,111 +0,0 @@
package com.xscm.moduleutil.bean.room.refining
data class BoxJiangChiBean(
var gid: String = "",
var gift_name: String = "",
var gift_price: String = "",
var base_image: String = "",
var play_image: String = ""
)
data class MonsterLogBean(
val add_time: Int,
val id: Int,
val is_evil_wind: Int,
val is_join: Int,
val win_type: Int,
val win_type_data: List<WinTypeData>,
val join_data: List<WinTypeData>,
val base_image: String,
val gift_name: String,
val gift_price: Int,
val num: Int,
val type_name: String,
)
data class WinTypeData(
val type: Int, val type_name: String,
val num: Int
)
data class MonsterUserLogBean(
val add_time: Int,
val base64_nick_name: String,
val base_image: String,
val gift_name: String,
val gift_price: String,
val head_pic: String,
val id: Int,
val mid: Int,
val nick_name: String,
val num: Int,
val type_name: String,
val uid: Int,
val win_type: Int
)
data class MonsterInfoBean(
val integral: String,
val is_finsh: Int,
val multiple_list: List<Multiple>,
val open_monster_price: String,
val surplus_time: Int,
val win_number: Int
)
data class Multiple(
val id: Int,
val multiple: Double,
val type_name: String,
val num: Int,
val type: Int
)
data class MonsterResultBean(
val head_pic: String,
val id: String,
val integral: String,
val nick_name: String,
val num: Int,
val price: Int,
val type: String,
val type_name: String,
val uid: Int
)
data class MonsterEndBean(
val game_name: String,
val gift_name: String,
val is_finsh: Int,
val is_push_message: Int,
val multiple_list: List<Multiple>,
val num: Int,
val surplus_time: Int,
val total_gift_price: Int,
val win_count: Int,
val win_number: Int,
val win_type_name: String,
val base_image: String
)
data class OpenMonsterBean(
val base_image: String, // 礼物图片
val gid: Int, // 礼物id
val gift_name: String, // 礼物名称
val gift_price: String, // 礼物价格
val is_win: Int,// 是否中奖 2未中奖 1中奖
val num: Int, // 中奖数量
val total_gift_price: Int, // 总价格
val type_name: String, // 中奖类型
val win_type: Int // 中奖类型
)

View File

@@ -1,194 +0,0 @@
package com.xscm.moduleutil.dialog;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.text.SpannableString;
import android.widget.TextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.color.ThemeableDrawableUtils;
import com.xscm.moduleutil.utils.ColorManager;
/**
*@author qx
*@data 2025/6/16
*@description: 确认弹框,使用在动态删除评论
*/
public class ConfirmDialog extends Dialog {
private String title;
private String message;
private SpannableString spannableMessage;
private String positiveButtonText;
private String negativeButtonText;
private View.OnClickListener positiveButtonClickListener;
private View.OnClickListener negativeButtonClickListener;
private boolean isCountdownEnabled = false; // 是否启用倒计时
private int countdownSeconds = 0; // 倒计时秒数
private CountDownTimer countDownTimer; // 倒计时对象
private Context context;
public ConfirmDialog(Context context, String title, String message,
String positiveButtonText, String negativeButtonText,
View.OnClickListener positiveButtonClickListener,
View.OnClickListener negativeButtonClickListener,
boolean isCountdownEnabled, int countdownSeconds) {
super(context);
this.context = context;
this.title = title;
this.message = message;
this.positiveButtonText = positiveButtonText;
this.negativeButtonText = negativeButtonText;
this.positiveButtonClickListener = positiveButtonClickListener;
this.negativeButtonClickListener = negativeButtonClickListener;
this.isCountdownEnabled = isCountdownEnabled;
this.countdownSeconds = countdownSeconds;
}
// 新增构造函数支持SpannableString
public ConfirmDialog(Context context, String title, SpannableString spannableMessage,
String positiveButtonText, String negativeButtonText,
View.OnClickListener positiveButtonClickListener,
View.OnClickListener negativeButtonClickListener,
boolean isCountdownEnabled, int countdownSeconds) {
super(context);
this.context = context;
this.title = title;
this.spannableMessage = spannableMessage;
this.positiveButtonText = positiveButtonText;
this.negativeButtonText = negativeButtonText;
this.positiveButtonClickListener = positiveButtonClickListener;
this.negativeButtonClickListener = negativeButtonClickListener;
this.isCountdownEnabled = isCountdownEnabled;
this.countdownSeconds = countdownSeconds;
}
private void init() {
Window window = getWindow();
if (window != null) {
window.setGravity(Gravity.CENTER); // 居中显示
window.setBackgroundDrawableResource(R.drawable.bg_r16_fff); // 透明背景
}
setCanceledOnTouchOutside(false); // 设置点击外部不取消对话框
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_confirm);
init();
// 动态设置背景
// getWindow().setBackgroundDrawableResource(R.drawable.bg_r16_fff);
// 初始化视图
TextView tvTitle = findViewById(R.id.tv_title);
TextView tvMessage = findViewById(R.id.tv_message);
Button btnPositive = findViewById(R.id.btn_positive);
Button btnNegative = findViewById(R.id.btn_negative);
@SuppressLint({"MissingInflatedId", "LocalSuppress"})
ConstraintLayout rootView =(ConstraintLayout) findViewById(R.id.root_view);
// 设置文本
tvTitle.setText(title);
// 根据是否有spannableMessage来设置不同的文本
if (spannableMessage != null) {
tvMessage.setText(spannableMessage);
} else {
tvMessage.setText(message);
}
btnPositive.setText(positiveButtonText);
btnNegative.setText(negativeButtonText);
// 设置点击监听器
btnPositive.setOnClickListener(v -> {
isCountdownCancelled = true; // 标记倒计时被取消
if (countDownTimer != null) {
countDownTimer.cancel(); // 取消倒计时
countDownTimer = null;
}
if (positiveButtonClickListener != null) {
positiveButtonClickListener.onClick(v);
}
dismiss();
});
// 判断是否需要显示取消按钮
if (negativeButtonText != null && !negativeButtonText.isEmpty()) {
btnNegative.setText(negativeButtonText);
btnNegative.setVisibility(View.VISIBLE);
btnNegative.setOnClickListener(v -> {
if (negativeButtonClickListener != null) {
negativeButtonClickListener.onClick(v);
}
dismiss();
});
} else {
// 隐藏取消按钮
btnNegative.setVisibility(View.GONE);
}
//判断是否需要显示确认按钮
if (positiveButtonText != null && !positiveButtonText.isEmpty()) {
btnPositive.setText(positiveButtonText);
btnPositive.setVisibility(View.VISIBLE);
}else {
// 隐藏确认按钮
btnPositive.setVisibility(View.GONE);
}
// 倒计时逻辑
if (isCountdownEnabled && countdownSeconds > 0) {
startCountdown(btnNegative);
}
ThemeableDrawableUtils.setThemeableRoundedBackground(btnPositive, ColorManager.getInstance().getPrimaryColorInt(), 53);
btnPositive.setTextColor(ColorManager.getInstance().getButtonColorInt());
}
private boolean isCountdownCancelled = false; // 添加标志位
private void startCountdown(Button btnNegative) {
countDownTimer = new CountDownTimer(countdownSeconds * 1000L, 1000) {
@Override
public void onTick(long millisUntilFinished) {
int secondsLeft = (int) (millisUntilFinished / 1000);
btnNegative.setText(negativeButtonText + " (" + secondsLeft + "s)");
}
@Override
public void onFinish() {
// 检查是否被主动取消
if (!isCountdownCancelled) {
btnNegative.setText(negativeButtonText);
if (negativeButtonClickListener != null) {
negativeButtonClickListener.onClick(btnNegative); // 自动触发取消
}
dismiss();
}
}
}.start();
}
@Override
public void dismiss() {
if (countDownTimer != null) {
countDownTimer.cancel(); // 取消倒计时
countDownTimer = null;
}
if (context instanceof Activity) {
Activity activity = (Activity) context;
if (activity.isFinishing() || activity.isDestroyed()) {
return; // Don't try to dismiss if activity is gone
}
}
super.dismiss();
}
}

View File

@@ -1,72 +0,0 @@
package com.xscm.moduleutil.diff
import android.os.Bundle
import androidx.recyclerview.widget.DiffUtil
import com.xscm.moduleutil.bean.CircleListBean
class CircleListDiffCallback(
private val oldList: List<CircleListBean>?,
private val newList: List<CircleListBean>?
) :
DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList?.size ?: 0
}
override fun getNewListSize(): Int {
return newList?.size ?: 0
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList!![oldItemPosition].id == newList!![newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList!![oldItemPosition]
val newItem = newList!![newItemPosition]
// 判断核心数据是否一致(排除动态字段)
if (!equalsStr(oldItem.nickname, newItem.nickname)) return false
if (!equalsStr(oldItem.avatar, newItem.avatar)) return false
if (!equalsStr(oldItem.content, newItem.content)) return false
if (oldItem.is_like != newItem.is_like) return false
if (oldItem.sex != newItem.sex) return false
if (!equalsStr(oldItem.images, newItem.images)) return false
// 动态字段不参与整体比较(让 getChangePayload 控制)
return equalsStr(oldItem.rewards_num, newItem.rewards_num)
&& equalsStr(oldItem.like_num, newItem.like_num)
&& equalsStr(oldItem.comment_num, newItem.comment_num)
// && equalsStr(oldItem.read_num, newItem.read_num)
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldList!![oldItemPosition]
val newItem = newList!![newItemPosition]
val diff = Bundle()
if (!equalsStr(oldItem.rewards_num, newItem.rewards_num)) {
diff.putString("rewards_num", newItem.rewards_num)
}
if (!equalsStr(oldItem.like_num, newItem.like_num)) {
diff.putString("like_num", newItem.like_num)
diff.putInt("is_like",newItem.is_like)
}
if (!equalsStr(oldItem.comment_num, newItem.comment_num)) {
diff.putString("comment_num", newItem.comment_num)
}
// if (!equalsStr(oldItem.read_num, newItem.read_num)) {
// diff.putString("read_num", newItem.read_num)
// }
return if (diff.size() == 0) null else diff
}
private fun equalsStr(a: String?, b: String?): Boolean {
if (a == null && b == null) return true
if (a == null || b == null) return false
return a == b
}
}

View File

@@ -1,41 +0,0 @@
package com.xscm.moduleutil.enumType
/**
* 房间类型枚举
* 对应 type_id 含义:
* 2-拍卖真爱拍小黑屋1/3/4/8-交友6-小黑屋7-互娱
*/
enum class RoomType(
val description: String, // 房间类型描述
private vararg val typeIds: Int // 该类型对应的所有 type_id支持多个
) {
// 枚举常量:名称(描述, 对应的type_id)
// SINGING_PK("点唱(pk)", 1),
AUCTION("拍卖(真爱拍小黑屋)", 2),
DATING("交友", 1,3, 4, 8), // 1、3、4、8 均对应交友
BLACK_ROOM("小黑屋", 6),
JUKEBOX("点唱", 9),
PUB_ROOM("酒吧", 11),
PRIVATE_ROOM("酒吧交友小屋", 12),
MUTUAL_ENTERTAINMENT("互娱", 7),
SIGN_CONTRACT("签约", 10);
companion object {
/**
* 根据 type_id 字符串获取对应的枚举实例
* @param typeIdStr 房间类型字符串(如 "1"、"3"
* @return 匹配的 RoomType若无效则返回 null
*/
fun fromTypeId(typeIdStr: String?): RoomType? {
if (typeIdStr.isNullOrBlank()) return null
val typeId = try {
typeIdStr.toInt() // 转换为 Int处理数字字符串
} catch (e: NumberFormatException) {
return null // 非数字字符串视为无效
}
// 遍历所有枚举,匹配 type_id
return values().find { typeId in it.typeIds }
}
}
}

View File

@@ -1,12 +0,0 @@
package com.xscm.moduleutil.event;
import com.xscm.moduleutil.BaseEvent;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class CabinEvent extends BaseEvent {
private boolean joined;
}

View File

@@ -1,6 +0,0 @@
package com.xscm.moduleutil.event;
import com.xscm.moduleutil.BaseEvent;
public class GiftDoubleClickEvent extends BaseEvent {
}

View File

@@ -1,6 +0,0 @@
package com.xscm.moduleutil.event;
import com.xscm.moduleutil.BaseEvent;
public class LogOutEvent extends BaseEvent {
}

View File

@@ -1,16 +0,0 @@
package com.xscm.moduleutil.event;
import com.xscm.moduleutil.BaseEvent;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
*@author qx
*@data 2025/6/24
*@description: 背景音乐
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class MusicEvent extends BaseEvent {
}

View File

@@ -1,20 +0,0 @@
package com.xscm.moduleutil.event;
import com.xscm.moduleutil.BaseEvent;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@Data
@NoArgsConstructor
public class PlazaEvent extends BaseEvent {
public static final int DELETE_ZONE = 1009;
public static final int MODIFY_ZONE = 1008;
public static final int ADD_ZONE = 1007;
private int msgId;
private int type;//DELETE_ZONE : 删除 MODIFY_ZONE : 修改 ADD_ZONE : 新增
}

View File

@@ -1,6 +0,0 @@
package com.xscm.moduleutil.event;
import com.xscm.moduleutil.BaseEvent;
public class RoomOutEvent extends BaseEvent {
}

View File

@@ -1,14 +0,0 @@
package com.xscm.moduleutil.event;
import com.xscm.moduleutil.BaseEvent;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class RoomOwnerLeaveEvent extends BaseEvent {
private String room_id;
private String user_id;
private String emchat_username;
}

View File

@@ -1,61 +0,0 @@
package com.xscm.moduleutil.http;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.http.NetworkException;
import com.blankj.utilcode.util.ToastUtils;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Response;
/**
* 项目名称:羽声语音
* 时间2025/12/23 16:58
* 用途:网络状态检查拦截器,会在每次请求前检查网络状态。
*/
public class NetworkCheckInterceptor implements Interceptor {
private final Context context;
public NetworkCheckInterceptor(Context context) {
this.context = context.getApplicationContext(); // 使用 Application Context 避免内存泄漏
}
@Override
public Response intercept(Chain chain) throws IOException {
// 检查网络连接状态
if (!isNetworkAvailable()) {
// 如果没有网络,抛出我们自定义的异常
ToastUtils.showLong("网络连接不可用,请检查您的网络设置");
}
// 如果有网络,继续执行请求
return chain.proceed(chain.request());
}
/**
* 检查网络是否可用
* @return true if network is available, false otherwise.
*/
private boolean isNetworkAvailable() {
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
// 获取所有网络信息
NetworkInfo[] networkInfos = connectivityManager.getAllNetworkInfo();
if (networkInfos != null) {
for (NetworkInfo info : networkInfos) {
if (info.getState() == NetworkInfo.State.CONNECTED) {
return true;
}
}
}
}
return false;
}
}

View File

@@ -1,21 +0,0 @@
package com.xscm.moduleutil.listener
import com.xscm.moduleutil.bean.RoomGiftData
import com.xscm.moduleutil.bean.RoomMessageEvent
/**
* 这是cp的监听器方便在各个模块中调用
*/
interface CPListener {
/**
* 发送CP消息的处理函数
* @param gitData 礼物数据信息,包含房间礼物相关的数据
*/
fun onSendCpMsg(gitData: RoomGiftData)
/**
* 接收房间消息事件的处理函数
* @param roomMessageEvent 房间消息事件对象,包含消息相关的所有信息
*/
fun onReceiveMsg(roomMessageEvent: RoomMessageEvent)
}

View File

@@ -1,5 +0,0 @@
package com.xscm.moduleutil.listener
interface JoinRoomErrorListener {
fun onJoinRoomError(errorCode: Int, errorMsg: String)
}

View File

@@ -1,703 +0,0 @@
package com.xscm.moduleutil.listener;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import com.blankj.utilcode.util.GsonUtils;
import com.blankj.utilcode.util.LogUtils;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener;
import com.tencent.imsdk.v2.V2TIMCallback;
import com.tencent.imsdk.v2.V2TIMConversationListener;
import com.tencent.imsdk.v2.V2TIMGroupListener;
import com.tencent.imsdk.v2.V2TIMGroupMemberInfo;
import com.tencent.imsdk.v2.V2TIMManager;
import com.tencent.imsdk.v2.V2TIMMessage;
import com.tencent.imsdk.v2.V2TIMSendCallback;
import com.tencent.imsdk.v2.V2TIMSimpleMsgListener;
import com.tencent.imsdk.v2.V2TIMUserInfo;
import com.xscm.moduleutil.base.CommonAppContext;
import com.xscm.moduleutil.bean.HeadlineBean;
import com.xscm.moduleutil.bean.RoomMessageEvent;
import com.xscm.moduleutil.event.UnreadCountEvent;
import com.xscm.moduleutil.http.RetrofitClient;
import com.xscm.moduleutil.utils.CustomMsgCode;
import com.xscm.moduleutil.utils.SpUtil;
import org.greenrobot.eventbus.EventBus;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
/**
* @author qx
* @data 2025/6/17
* @description: 接收消息
*/
public class MessageExListenerSingleton {
private static boolean isInitialized = false;
private static MessageExListenerSingleton instance;
private List<OnMessageReceivedListener> listeners = new ArrayList<>();
private V2TIMSimpleMsgListener simpleMsgListener;
private V2TIMAdvancedMsgListener v2TIMAdvancedMsgListener;
private static String mRoomId = "";
public static String groupId;
private V2TIMGroupListener groupListener;
private V2TIMConversationListener conversationListener; // 需要保存引用
// 添加操作状态标记
private volatile boolean isGroupOperationInProgress = false;
private final Object groupOperationLock = new Object();
private Handler mainHandler = new Handler(Looper.getMainLooper());
// 添加消息缓存机制
private static final int MAX_CACHED_MESSAGES = 20;
private final Map<String, List<RoomMessageEvent>> cachedMessages = new ConcurrentHashMap<>();
private final Set<String> joinedRooms = ConcurrentHashMap.newKeySet();
private OnMsgTaskListener onMsgTaskListener;
// private boolean listenersAdded = false; // 标记监听器是否已添加
// 1. 添加新的监听器接口
public interface PublicScreenMessageListener {
void onPublicScreenMessageReceived(RoomMessageEvent message);
}
private List<PublicScreenMessageListener> publicScreenListeners = new ArrayList<>();
// 添加监听器 // 替换原有的 addPublicScreenMessageListener 方法
public void addPublicScreenMessageListener(PublicScreenMessageListener listener) {
if (listener == null) {
return;
}
synchronized (publicScreenListeners) {
if (!publicScreenListeners.contains(listener)) {
try {
publicScreenListeners.add(listener);
} catch (Exception e) {
LogUtils.e("MessageListener", "添加 PublicScreenMessageListener 失败: " + e.getMessage());
}
}
}
}
// 同时修改 removePublicScreenMessageListener 方法
public void removePublicScreenMessageListener(PublicScreenMessageListener listener) {
if (listener == null) {
return;
}
synchronized (publicScreenListeners) {
try {
publicScreenListeners.remove(listener);
} catch (Exception e) {
LogUtils.e("MessageListener", "移除 PublicScreenMessageListener 失败: " + e.getMessage());
}
}
}
// 修改 notify 方法以增加保护
private void notifyPublicScreenListeners(RoomMessageEvent message) {
synchronized (publicScreenListeners) {
// 创建副本以避免并发修改异常
List<PublicScreenMessageListener> listenersCopy = new ArrayList<>(publicScreenListeners);
for (PublicScreenMessageListener listener : listenersCopy) {
try {
listener.onPublicScreenMessageReceived(message);
} catch (Exception e) {
LogUtils.e("MessageListener", "通知 PublicScreenMessageListener 失败: " + e.getMessage());
}
}
}
}
private MessageExListenerSingleton() {
if (!isInitialized) {
isInitialized = true;
}
}
public static MessageExListenerSingleton getInstance() {
synchronized (MessageExListenerSingleton.class) {
if (instance == null) {
instance = new MessageExListenerSingleton();
}
return instance;
}
}
/**
* 缓存消息用于在Fragment未准备好时存储消息
*/
private void cacheMessage(String roomId, RoomMessageEvent message) {
if (TextUtils.isEmpty(roomId) || message == null) {
return;
}
// 标记该房间有待处理的消息
List<RoomMessageEvent> roomMessages = cachedMessages.computeIfAbsent(roomId, k -> new ArrayList<>());
// 限制每个房间的缓存消息数量
if (roomMessages.size() >= MAX_CACHED_MESSAGES) {
roomMessages.remove(0); // 移除最旧的消息
}
roomMessages.add(message);
LogUtils.d("MessageListener", "缓存消息: roomId=" + roomId + ", msgType=" + message.getMsgType());
}
/**
* 获取并清除指定房间的缓存消息
*/
public List<RoomMessageEvent> getAndClearCachedMessages(String roomId) {
if (TextUtils.isEmpty(roomId)) {
return new ArrayList<>();
}
List<RoomMessageEvent> messages = cachedMessages.remove(roomId);
if (messages == null) {
messages = new ArrayList<>();
}
LogUtils.d("MessageListener", "获取并清除缓存消息: roomId=" + roomId + ", count=" + messages.size());
return messages;
}
/**
* 标记房间已加入
*/
public void markRoomJoined(String roomId) {
if (!TextUtils.isEmpty(roomId)) {
joinedRooms.add(roomId);
LogUtils.d("MessageListener", "标记房间已加入: " + roomId);
}
}
/**
* 检查房间是否已加入
*/
public boolean isRoomJoined(String roomId) {
return !TextUtils.isEmpty(roomId) && joinedRooms.contains(roomId);
}
/**
* 清除房间加入标记
*/
public void clearRoomJoined(String roomId) {
if (!TextUtils.isEmpty(roomId)) {
joinedRooms.remove(roomId);
LogUtils.d("MessageListener", "清除房间加入标记: " + roomId);
}
}
// 修改 joinGroup 方法,确保先退出再加入
public void joinGroup(String roomId) {
if (TextUtils.isEmpty(roomId)) {
return;
}
synchronized (groupOperationLock) {
if (isGroupOperationInProgress) {
// 如果有操作正在进行,延迟执行
mainHandler.removeCallbacksAndMessages(null);
mainHandler.postDelayed(() -> joinGroup(roomId), 100);
return;
}
isGroupOperationInProgress = true;
}
mRoomId = roomId;
new Thread(() -> {
try {
// 先退出当前群组(如果需要)
if (groupId != null && !groupId.equals(roomId)) {
LogUtils.d("MessageListener", "开始退出群组: " + groupId + "____room:" + roomId);
CountDownLatch quitLatch = new CountDownLatch(1);
boolean[] quitSuccess = {false};
try {
V2TIMManager.getInstance().quitGroup("room" + groupId, new V2TIMCallback() {
@Override
public void onSuccess() {
LogUtils.d("MessageListener", "退出群组成功: " + groupId + "____room:" + roomId);
quitSuccess[0] = true;
quitLatch.countDown();
}
@Override
public void onError(int code, String desc) {
LogUtils.e("MessageListener", "退出群组失败: " + groupId + "____room:" + roomId + ", code=" + code + ", desc=" + desc);
quitSuccess[0] = false;
quitLatch.countDown();
}
});
// 等待退出操作完成最多等待3秒
try {
quitLatch.await(3, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} catch (Exception e) {
LogUtils.e("MessageListener", "退出群组异常: " + e.getMessage());
}
}
// 加入新群组
LogUtils.d("MessageListener", "开始加入群组: " + roomId);
CountDownLatch joinLatch = new CountDownLatch(1);
boolean[] joinSuccess = {false};
try {
// 确保监听器已添加
ensureListenersAdded();
V2TIMManager.getInstance().joinGroup("room" + roomId, "申请加入", new V2TIMCallback() {
@Override
public void onSuccess() {
LogUtils.d("MessageListener", "加入im群组成功: " + roomId);
joinSuccess[0] = true;
groupId = roomId;
joinLatch.countDown();
// 标记房间已加入
markRoomJoined(roomId);
}
@Override
public void onError(int code, String desc) {
LogUtils.e("MessageListener", "加入群组失败: " + roomId + ", code=" + code + ", desc=" + desc);
joinSuccess[0] = false;
joinLatch.countDown();
}
});
// 等待加入操作完成最多等待3秒
try {
joinLatch.await(3, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} catch (Exception e) {
LogUtils.e("MessageListener", "加入群组异常: " + e.getMessage());
}
LogUtils.d("MessageListener", "群组操作完成 - 退出成功: " + (groupId == null || !groupId.equals(roomId)) + ", 加入成功: " + joinSuccess[0]);
} finally {
synchronized (groupOperationLock) {
isGroupOperationInProgress = false;
}
}
}).start();
}
private void initListeners() {
// 简单消息监听器
if (simpleMsgListener == null) {
simpleMsgListener = new V2TIMSimpleMsgListener() {
@Override
public void onRecvC2CTextMessage(String msgID, V2TIMUserInfo sender, String text) {
LogUtils.d("C2C 文本消息 " + sender.getNickName());
}
@Override
public void onRecvC2CCustomMessage(String msgID, V2TIMUserInfo sender, byte[] customData) {
LogUtils.d("C2C 自定义(信令)消息 " + sender.getNickName());
String message = new String(customData, StandardCharsets.UTF_8);
RoomMessageEvent event = GsonUtils.fromJson(message, RoomMessageEvent.class);
if (event.getMsgType() == 130 || event.getMsgType() == 131) {
// EventBus.getDefault().post(event);
RetrofitClient.getInstance().getCpListener().onReceiveMsg(event);
} else if (event.getMsgType() == CustomMsgCode.INSTANCE.getCODE_TASK_APPRENTICE_JOIN_ROOM()) {
if (onMsgTaskListener != null) {
onMsgTaskListener.onMsgTask(event);
}
} else {
notifyMessageReceived(event);
}
}
@Override
public void onRecvGroupTextMessage(String msgID, String groupID, V2TIMGroupMemberInfo sender, String text) {
LogUtils.d("群文本消息:群组 " + groupID + "" + sender.getNickName());
}
@Override
public void onRecvGroupCustomMessage(String msgID, String groupID, V2TIMGroupMemberInfo sender, byte[] customData) {
LogUtils.d("收到群自定义消息:群组 " + groupID + "" + sender.getNickName() +",mRoomId:"+mRoomId);
if (!groupID.equals("")) {
if (groupID.replace("room","").equals(mRoomId)) {
String message = new String(customData, StandardCharsets.UTF_8);
RoomMessageEvent event = GsonUtils.fromJson(message, RoomMessageEvent.class);
notifyMessageReceived(event);
LogUtils.d("收到群自定义消息(信令):", message);
}
} else {
String message = new String(customData, StandardCharsets.UTF_8);
LogUtils.d("收到群自定义消息(信令):", message);
HeadlineBean event = GsonUtils.fromJson(message, HeadlineBean.class);
EventBus.getDefault().post(event);
}
}
};
}
// 高级消息监听器
if (v2TIMAdvancedMsgListener == null) {
v2TIMAdvancedMsgListener = new V2TIMAdvancedMsgListener() {
@Override
public void onRecvNewMessage(V2TIMMessage msg) {
super.onRecvNewMessage(msg);
if (msg.isBroadcastMessage()) {
// 收到了广播消息
String message = new String(msg.getCustomElem().getData(), StandardCharsets.UTF_8);
LogUtils.e("收到广播消息(系统):", message);
}
}
};
}
// 群组监听器
if (groupListener == null) {
groupListener = new V2TIMGroupListener() {
@Override
public void onMemberEnter(String groupID, List<V2TIMGroupMemberInfo> memberList) {
// 有新成员加入群,该群所有的成员都能收到
}
@Override
public void onMemberLeave(String groupID, V2TIMGroupMemberInfo member) {
// 有成员离开群,该群所有的成员都能收到
}
@Override
public void onReceiveRESTCustomData(String groupID, byte[] customData) {
String message = "";
try {
LogUtils.e("收到群自定义消息",groupID);
message = new String(customData, StandardCharsets.UTF_8);
LogUtils.e("收到群自定义消息(系统)" + message);
} catch (Exception e) {
// 处理转换过程中可能出现的异常,例如记录日志
LogUtils.e("转换 customData 为 String 时出错:" + e.getMessage());
return; // 退出方法,避免后续代码执行
}
RoomMessageEvent event = null;
try {
// 特殊处理某些消息类型
event = parseSpecialMessageTypes(message);
if (event == null) {
// 使用默认解析
event = GsonUtils.fromJson(message, RoomMessageEvent.class);
}
LogUtils.e("收到群自定义消息:" + mRoomId + "===" + event);
// event = GsonUtils.fromJson(message, RoomMessageEvent.class);
} catch (Exception e) {
// 处理 JSON 解析过程中可能出现的异常,例如记录日志
LogUtils.e("解析 JSON 数据时出错:" + e.getMessage());
return; // 退出方法,避免后续代码执行
}
if (groupID.contains(mRoomId)) {
notifyMessageReceived(event);
}
}
};
}
// 会话监听器
if (conversationListener == null) {
conversationListener = new V2TIMConversationListener() {
@Override
public void onTotalUnreadMessageCountChanged(long totalUnreadCount) {
super.onTotalUnreadMessageCountChanged(totalUnreadCount);
UnreadCountEvent event = CommonAppContext.getInstance().getUnreadCountEvent();
if (event == null) {
event = new UnreadCountEvent();
event.setBLong(0);
}
event.setALong(totalUnreadCount);
CommonAppContext.getInstance().setUnreadCountEvent(event);
EventBus.getDefault().post(event);
}
};
}
}
// 添加所有监听器
private void addAllListeners() {
if (simpleMsgListener != null) {
V2TIMManager.getInstance().addSimpleMsgListener(simpleMsgListener);
}
if (v2TIMAdvancedMsgListener != null) {
V2TIMManager.getMessageManager().addAdvancedMsgListener(v2TIMAdvancedMsgListener);
}
if (groupListener != null) {
V2TIMManager.getInstance().addGroupListener(groupListener);
}
if (conversationListener != null) {
V2TIMManager.getConversationManager().addConversationListener(conversationListener);
}
}
// 移除所有监听器
private static void removeAllListeners() {
if (instance != null) {
if (instance.simpleMsgListener != null) {
V2TIMManager.getInstance().removeSimpleMsgListener(instance.simpleMsgListener);
}
if (instance.v2TIMAdvancedMsgListener != null) {
V2TIMManager.getMessageManager().removeAdvancedMsgListener(instance.v2TIMAdvancedMsgListener);
}
if (instance.groupListener != null) {
V2TIMManager.getInstance().removeGroupListener(instance.groupListener);
}
if (instance.conversationListener != null) {
V2TIMManager.getConversationManager().removeConversationListener(instance.conversationListener);
}
// instance.listenersAdded = false; // 重置标记
}
}
// 修改 quitGroup 方法
public static void quitGroup(String mRoomId) {
V2TIMManager.getInstance().quitGroup("room" + mRoomId, new V2TIMCallback() {
@Override
public void onSuccess() {
LogUtils.d("@@@", "退出群组成功" + mRoomId);
// removeAllListeners(); // 移除所有监听器
}
@Override
public void onError(int code, String desc) {
LogUtils.d("@@@", "退出群组失败" + mRoomId, code, desc);
// removeAllListeners(); // 即使失败也移除监听器
}
});
if (instance != null) {
instance.listeners.clear();
// removeAllListeners();
isInitialized = false;
groupId = null;
LogUtils.e("@@@", "重置成功");
}
}
// 修改 reset 方法
public static void reset(String roomId) {
if (instance != null) {
instance.listeners.clear();
// removeAllListeners();
isInitialized = false;
groupId = null;
// instance = null;
quitGroup(roomId);
LogUtils.e("@@@", "重置成功");
}
}
// 确保监听器已添加
public void ensureListenersAdded() {
initListeners();
addAllListeners();
}
private RoomMessageEvent parseSpecialMessageTypes(String message) {
try {
JsonObject jsonObject = new JsonParser().parse(message).getAsJsonObject();
int msgType = jsonObject.get("MsgType").getAsInt();
if (msgType == 1053) {
return CustomMessageParser.parseMessageType1053(jsonObject);
} else if (msgType == 1054) {
return CustomMessageParser.parseMessageType1054(jsonObject);
}
return null; // 不是特殊类型,使用默认解析
} catch (Exception e) {
LogUtils.e("特殊消息解析失败: " + e.getMessage());
return null;
}
}
public void addOnMessageReceivedListener(OnMessageReceivedListener listener) {
listeners.add(listener);
}
/**
* 移除群组相关的监听器
*/
private static void removeGroupListeners() {
if (instance != null) {
// 移除消息监听器
if (instance.simpleMsgListener != null) {
V2TIMManager.getInstance().removeSimpleMsgListener(instance.simpleMsgListener);
}
if (instance.v2TIMAdvancedMsgListener != null) {
V2TIMManager.getMessageManager().removeAdvancedMsgListener(instance.v2TIMAdvancedMsgListener);
}
// 移除群组监听器
if (instance.groupListener != null) {
V2TIMManager.getInstance().removeGroupListener(instance.groupListener);
}
}
}
public void sendCustomRoomMessage(String roomId, byte[] binaryData) {
// 创建自定义群消息
V2TIMMessage v2TIMMessage = V2TIMManager.getMessageManager().createCustomMessage(binaryData);
// v2TIMMessage.setNeedReadReceipt(true);
// 发送消息
V2TIMManager.getMessageManager().sendMessage(
v2TIMMessage,
null,
"room" + roomId,
V2TIMMessage.V2TIM_PRIORITY_NORMAL,
false,
null,
sendCallback
);
}
// TODO: 2025/11/19 添加发送公共方法messageType发送的typemessage发送的内容 userId发送给谁
public void sendCustomC2CMessage(int messageType, String message, String userId) {
RoomMessageEvent.T t = new RoomMessageEvent.T();
t.setFromUserInfo(SpUtil.getUserInfo());
t.setText(message);
RoomMessageEvent roomMessageEvent = new RoomMessageEvent(messageType, mRoomId, t);
String json = GsonUtils.toJson(roomMessageEvent);
// 转换为 byte[]
byte[] binaryData = json.getBytes(StandardCharsets.UTF_8);
// 创建自定义群消息
V2TIMMessage v2TIMMessage = V2TIMManager.getMessageManager().createCustomMessage(binaryData);
v2TIMMessage.setExcludedFromUnreadCount(true);
v2TIMMessage.setExcludedFromContentModeration(true);
// v2TIMMessage.setNeedReadReceipt(true);
//
// // 发送消息
V2TIMManager.getMessageManager().sendMessage(v2TIMMessage, "u" + userId, null, V2TIMMessage.V2TIM_PRIORITY_HIGH,
true,
null,
sendCallback);
}
// TODO: 2025/11/19 添加发送公共方法messageType发送的typemessage发送的内容 userId发送给谁
public void sendCustomC2CMessage(int messageType, String userId,RoomMessageEvent.T text) {
text.setFromUserInfo(SpUtil.getUserInfo());
LogUtils.e("发送消息", "messageType:" + messageType + "\nuserId:" + userId + "\ntext:" + text,toString());
RoomMessageEvent roomMessageEvent = new RoomMessageEvent(messageType, mRoomId, text);
String json = GsonUtils.toJson(roomMessageEvent);
// 转换为 byte[]
byte[] binaryData = json.getBytes(StandardCharsets.UTF_8);
// 创建自定义群消息
V2TIMMessage v2TIMMessage = V2TIMManager.getMessageManager().createCustomMessage(binaryData);
v2TIMMessage.setExcludedFromUnreadCount(true);
v2TIMMessage.setExcludedFromContentModeration(true);
// v2TIMMessage.setNeedReadReceipt(true);
//
// // 发送消息
V2TIMManager.getMessageManager().sendMessage(v2TIMMessage, "u" + userId, null, V2TIMMessage.V2TIM_PRIORITY_HIGH,
true,
null,
sendCallback);
}
public void sendCustomC2CMessage125(String userId, byte[] binaryData) {
// 创建自定义群消息
V2TIMMessage v2TIMMessage = V2TIMManager.getMessageManager().createCustomMessage(binaryData);
v2TIMMessage.setExcludedFromUnreadCount(true);
v2TIMMessage.setExcludedFromContentModeration(true);
// v2TIMMessage.setNeedReadReceipt(true);
//
// // 发送消息
V2TIMManager.getMessageManager().sendMessage(v2TIMMessage, "u" + userId, null, V2TIMMessage.V2TIM_PRIORITY_HIGH,
true,
null,
sendCallback);
}
// RoomFragment.java 中添加
private final V2TIMSendCallback<V2TIMMessage> sendCallback = new V2TIMSendCallback<V2TIMMessage>() {
@Override
public void onProgress(int progress) {
// 可选实现
}
@Override
public void onSuccess(V2TIMMessage message) {
Log.d("MessageSender", "发送成功");
}
@Override
public void onError(int code, String desc) {
Log.e("MessageSender", "发送失败: code=" + code + ", desc=" + desc);
}
};
private void notifyMessageReceived(RoomMessageEvent message) {
// for (OnMessageReceivedListener listener : listeners) {
// listener.onMessageReceived(message);
// }
if ((message.getMsgType() == 1001 || message.getMsgType() == 1080) && publicScreenListeners.isEmpty()) {
cacheMessage(message.getRoomId(), message);
}
// 通知原有的监听器
for (OnMessageReceivedListener listener : listeners) {
listener.onMessageReceived(message);
}
// 通知 PublicScreenEaseChatFragment 监听器
for (PublicScreenMessageListener listener : publicScreenListeners) {
listener.onPublicScreenMessageReceived(message);
}
}
public interface OnMessageReceivedListener {
void onMessageReceived(RoomMessageEvent message);
}
public void setOnMsgTaskListener(OnMsgTaskListener listener) {
this.onMsgTaskListener = listener;
}
public interface OnMsgTaskListener {
void onMsgTask(RoomMessageEvent message);
}
}

View File

@@ -1,35 +0,0 @@
package com.xscm.moduleutil.utils;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.View;
/**
*@author qx
*@data 2025/9/10
*@description: 防止重复点击的工具类
*/
public class ClickUtils {
private static final long DEFAULT_CLICK_INTERVAL = 500;
private static final SparseArray<Long> lastClickTimes = new SparseArray<>();
public static boolean isFastDoubleClick(View view) {
return isFastDoubleClick(view, DEFAULT_CLICK_INTERVAL);
}
public static boolean isFastDoubleClick(View view, long interval) {
long currentTime = SystemClock.elapsedRealtime();
int viewId = view.getId();
Long lastClickTime = lastClickTimes.get(viewId);
if (lastClickTime != null && currentTime - lastClickTime < interval) {
return true;
}
lastClickTimes.put(viewId, currentTime);
return false;
}
// 新增:清理所有点击记录
public static void clearAllClickRecords() {
lastClickTimes.clear();
}
}

View File

@@ -1,62 +0,0 @@
package com.xscm.moduleutil.utils
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.text.DecimalFormat
object CountDownUtil {
/**
* 倒计时的实现
*/
fun countDown(lifecycleScope: LifecycleCoroutineScope,
time: Int = 5,
start: (scop: CoroutineScope) -> Unit,
end: () -> Unit,
next: (time: Int) -> Unit
) {
lifecycleScope.launch {
// 在这个范围内启动的协程会在Lifecycle被销毁的时候自动取消
flow {
(time downTo 0).forEach {
delay(1000)
emit(it)
}
}.onStart {
// 倒计时开始 在这里可以让Button 禁止点击状态
start(this@launch)
}.onCompletion {
// 倒计时结束 在这里可以让Button 恢复点击状态
end()
}.catch {
//错误
// YYLogUtils.e(it.message ?: "Unkown Error")
}.collect {
// 在这里 更新值来显示到UI
next(it)
}
}
}
fun formatCountDownTime(milliseconds: Long): String {
val sb = StringBuilder()
val mss = milliseconds * 1000 / 1000
val days = mss / (60 * 60 * 24)
val hours = mss % (60 * 60 * 24) / (60 * 60)
val minutes = mss % (60 * 60) / 60
val seconds = mss % 60
val format = DecimalFormat("00")
if (days > 0 || hours > 0) {
sb.append(format.format(hours)).append(":").append(format.format(minutes)).append(":")
.append(format.format(seconds))
} else {
sb.append(format.format(minutes)).append(":").append(format.format(seconds))
}
return sb.toString()
}
}

View File

@@ -1,10 +0,0 @@
package com.xscm.moduleutil.utils
object CustomMsgCode {
val CODE_TASK_APPRENTICE_JOIN_ROOM_TIMER_ID = "132"
val CODE_TASK_APPRENTICE_JOIN_ROOM = 132
val CODE_TASK_APPRENTICE_JOIN_ROOM_MSG = "您的师傅邀请您进入房间,您是否同意?"
val CODE_TASK_APPRENTICE_JOIN_ROOM_MSG_REFUSE = "您的徒弟拒绝了您的邀请。"
val CODE_USER_SIGN_IN = "签到"
}

View File

@@ -1,300 +0,0 @@
package com.xscm.moduleutil.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.LogUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.signature.ObjectKey;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.widget.GrayscaleTransformation;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* Copyright (c) 1
*
*/
public class ImageLoader {
private static RequestOptions createUrlOnlyOptions(String url) {
String urls = String.valueOf(new GlideUrl(url));
return new RequestOptions().signature(new ObjectKey(urls)).diskCacheStrategy(DiskCacheStrategy.ALL)
.skipMemoryCache(false)
.format(DecodeFormat.PREFER_RGB_565);
// 禁止尺寸影响缓存
// .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
}
public static void loadHead(Context context, ImageView view, String url) {
RequestOptions options = RequestOptions.circleCropTransform();
Glide.with(context).load(url).apply(options).apply(createUrlOnlyOptions(url)).error(com.xscm.moduleutil.R.mipmap.default_avatar).placeholder(R.mipmap.default_avatar)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
// dataSource 是关键参数!
switch (dataSource) {
case DATA_DISK_CACHE:
case RESOURCE_DISK_CACHE:
LogUtils.e("GlideCache", "来自磁盘缓存");
break;
case MEMORY_CACHE:
LogUtils.e("GlideCache", "来自内存缓存");
break;
case LOCAL: // 本地文件
LogUtils.e("GlideCache", "来自本地文件");
break;
case REMOTE: // 网络下载
LogUtils.e("GlideCache", "来自网络下载");
break;
default:
LogUtils.e("GlideCache", "来自: " + dataSource);
}
return false;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
Log.e("GlideCache", "加载失败"+"path:", e);
return false;
}
}).thumbnail(0.3f).into(view);
}
public static void loadHead(ImageView view, String url) {
RequestOptions options = RequestOptions.circleCropTransform();
Glide.with(view).load(url).apply(options).apply(createUrlOnlyOptions(url)).error(R.mipmap.default_avatar).placeholder(R.mipmap.default_avatar)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
// dataSource 是关键参数!
switch (dataSource) {
case DATA_DISK_CACHE:
case RESOURCE_DISK_CACHE:
LogUtils.e("GlideCache", "来自磁盘缓存");
break;
case MEMORY_CACHE:
LogUtils.e("GlideCache", "来自内存缓存");
break;
case LOCAL: // 本地文件
LogUtils.e("GlideCache", "来自本地文件");
break;
case REMOTE: // 网络下载
LogUtils.e("GlideCache", "来自网络下载");
break;
default:
LogUtils.e("GlideCache", "来自: " + dataSource);
}
return false;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
Log.e("GlideCache", "加载失败"+"path:", e);
return false;
}
}).thumbnail(0.3f).into(view);
}
public static void loadImage(ImageView view, String url) {
RequestOptions options = RequestOptions.circleCropTransform();
Glide.with(view).load(url).apply(options).apply(createUrlOnlyOptions(url)).error(R.mipmap.default_image).placeholder(R.mipmap.default_image)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
// dataSource 是关键参数!
switch (dataSource) {
case DATA_DISK_CACHE:
case RESOURCE_DISK_CACHE:
LogUtils.e("GlideCache", "来自磁盘缓存");
break;
case MEMORY_CACHE:
LogUtils.e("GlideCache", "来自内存缓存");
break;
case LOCAL: // 本地文件
LogUtils.e("GlideCache", "来自本地文件");
break;
case REMOTE: // 网络下载
LogUtils.e("GlideCache", "来自网络下载");
break;
default:
LogUtils.e("GlideCache", "来自: " + dataSource);
}
return false;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
Log.e("GlideCache", "加载失败"+"path:", e);
return false;
}
}).thumbnail(0.3f).into(view);
}
public static void loadImage(Context context, ImageView view, String url) {
Glide.with(context).load(url).apply(createUrlOnlyOptions(url)).error(R.mipmap.default_image).placeholder(R.mipmap.default_image)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
// dataSource 是关键参数!
switch (dataSource) {
case DATA_DISK_CACHE:
case RESOURCE_DISK_CACHE:
LogUtils.e("GlideCache", "来自磁盘缓存");
break;
case MEMORY_CACHE:
LogUtils.e("GlideCache", "来自内存缓存");
break;
case LOCAL: // 本地文件
LogUtils.e("GlideCache", "来自本地文件");
break;
case REMOTE: // 网络下载
LogUtils.e("GlideCache", "来自网络下载");
break;
default:
LogUtils.e("GlideCache", "来自: " + dataSource);
}
return false;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
Log.e("GlideCache", "加载失败"+"path:", e);
return false;
}
}).thumbnail(0.3f).into(view);
}
/**
* 加载图片并灰度
* @param context
* @param view
* @param url
* @param placeholder
*/
public static void loadImage(Context context,ImageView view, String url, Float placeholder) {
Glide.with(context).load(url).apply(RequestOptions.bitmapTransform(new GrayscaleTransformation(placeholder)))
.error(R.mipmap.default_image).placeholder(R.mipmap.default_image).diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
// dataSource 是关键参数!
switch (dataSource) {
case DATA_DISK_CACHE:
case RESOURCE_DISK_CACHE:
LogUtils.e("GlideCache", "来自磁盘缓存");
break;
case MEMORY_CACHE:
LogUtils.e("GlideCache", "来自内存缓存");
break;
case LOCAL: // 本地文件
LogUtils.e("GlideCache", "来自本地文件");
break;
case REMOTE: // 网络下载
LogUtils.e("GlideCache", "来自网络下载");
break;
default:
LogUtils.e("GlideCache", "来自: " + dataSource);
}
return false;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
Log.e("GlideCache", "加载失败"+"path:", e);
return false;
}
}).thumbnail(0.3f).into(view);
}
// 可调节灰度程度的方法
public static void loadGrayscaleImage(Context context, String url, ImageView imageView, float saturation) {
Glide.with(context)
.load(url)
.apply(RequestOptions.bitmapTransform(new GrayscaleTransformation(saturation)))
.listener(new RequestListener<Drawable>() {
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
// dataSource 是关键参数!
switch (dataSource) {
case DATA_DISK_CACHE:
case RESOURCE_DISK_CACHE:
LogUtils.e("GlideCache", "来自磁盘缓存");
break;
case MEMORY_CACHE:
LogUtils.e("GlideCache", "来自内存缓存");
break;
case LOCAL: // 本地文件
LogUtils.e("GlideCache", "来自本地文件");
break;
case REMOTE: // 网络下载
LogUtils.e("GlideCache", "来自网络下载");
break;
default:
LogUtils.e("GlideCache", "来自: " + dataSource);
}
return false;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
Log.e("GlideCache", "加载失败"+"path:", e);
return false;
}
})
.into(imageView);
}
public static void loadIcon(Context context, ImageView view, String url) {
if (TextUtils.isEmpty(url)) {
view.setVisibility(View.GONE);
} else {
view.setVisibility(View.VISIBLE);
}
Glide.with(context).load(url).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(view);
}
}

View File

@@ -1,136 +0,0 @@
package com.xscm.moduleutil.utils;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.blankj.utilcode.util.LogUtils;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.utils.logger.Logger;
import com.xscm.moduleutil.widget.AvatarFrameView;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
/**
* 描述 设置中的用户头像
*/
public class MeHeadView extends ConstraintLayout {
private ImageView mRiv;
private AvatarFrameView mIvFrame;
private ImageView mIvSex;
private ImageView mIvOnline;
private ImageView iv_frame_bg;
private ImageView iv_master_bg;
public MeHeadView(Context context) {
this(context, null, 0);
}
public MeHeadView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MeHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs, defStyleAttr);
}
private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
LayoutInflater.from(context).inflate(R.layout.me_view_decoration_head, this, true);
mRiv = findViewById(R.id.riv);
mIvFrame = findViewById(R.id.iv_frame);
mIvSex = findViewById(R.id.iv_sex);
mIvOnline = findViewById(R.id.iv_online);
iv_frame_bg = findViewById(R.id.iv_frame_bg);
iv_master_bg = findViewById(R.id.iv_master_bg);
mIvSex.setVisibility(GONE);
}
public void setData(String headPicture, String framePicture, String nobilityImage) {
LogUtils.e(headPicture, framePicture, nobilityImage);
if (!TextUtils.isEmpty(headPicture)) {
ImageUtils.loadHeadCC(headPicture, mRiv);
}
if (TextUtils.isEmpty(framePicture)) {
mIvFrame.setVisibility(GONE);
} else {
mIvFrame.setVisibility(VISIBLE);
mIvFrame.stopPlay();
Observable.timer(200, TimeUnit.MILLISECONDS).subscribe(aLong -> {
mIvFrame.setSource(framePicture, 1);
});
}
if (nobilityImage != null && !TextUtils.isEmpty(nobilityImage)) {
iv_frame_bg.setVisibility(VISIBLE);
ImageUtils.loadRoomItem(nobilityImage, iv_frame_bg);
} else {
iv_frame_bg.setVisibility(GONE);
}
}
public void setMaster(String headPicture) {
if (!TextUtils.isEmpty(headPicture)) {
ImageUtils.loadHead(headPicture, mRiv);
}
iv_master_bg.setVisibility(VISIBLE);
iv_master_bg.setImageResource(R.mipmap.icon_master);
}
public void setOnline(boolean isOnline) {
mIvOnline.setVisibility(VISIBLE);
mIvOnline.setImageResource(isOnline ? R.mipmap.me_online_icon : R.mipmap.me_icon_unchecked);
}
public void setSex(int sex, String headPicture, String dress) {
if (!TextUtils.isEmpty(headPicture)) {
ImageUtils.loadHead(headPicture, mRiv);
}
if (TextUtils.isEmpty(dress)) {
mIvFrame.setVisibility(GONE);
} else {
mIvFrame.setVisibility(VISIBLE);
mIvFrame.setSource(dress, 1);
}
mIvSex.setVisibility(VISIBLE);
mIvSex.setImageResource(sex == 1 ? R.mipmap.nan : R.mipmap.nv);
}
/**
* 设置头像,性别,头像框,贵族
*
* @param sex
* @param headPicture
* @param framePicture
* @param nobilityImage
*/
public void setAll(int sex,String headPicture, String framePicture, String nobilityImage) {
if (!TextUtils.isEmpty(headPicture)) {
ImageUtils.loadHead(headPicture, mRiv);
}
if (TextUtils.isEmpty(framePicture)) {
mIvFrame.setVisibility(GONE);
} else {
mIvFrame.setVisibility(VISIBLE);
mIvFrame.setSource(framePicture, 1);
}
mIvSex.setImageResource(sex == 1 ? R.mipmap.nan : R.mipmap.nv);
if (nobilityImage != null && !TextUtils.isEmpty(nobilityImage)) {
iv_frame_bg.setVisibility(VISIBLE);
ImageUtils.loadRoomItem(nobilityImage, iv_frame_bg);
} else {
iv_frame_bg.setVisibility(GONE);
}
}
}

View File

@@ -1,21 +0,0 @@
package com.xscm.moduleutil.utils
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
class NullToEmptyStringAdapter : TypeAdapter<String>() {
override fun write(out: JsonWriter, value: String?) {
out.value(value ?: "")
}
override fun read(`in`: JsonReader): String {
return if (`in`.peek() == JsonToken.NULL) {
`in`.nextNull()
"" // null → 空字符串
} else {
`in`.nextString() ?: ""
}
}
}

View File

@@ -1,142 +0,0 @@
package com.xscm.moduleutil.utils
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import java.util.LinkedList
import java.util.Locale
/**
* 项目名称:羽声语音
* 时间2025/12/6 11:00
* 用途:
*/
class TTSManager private constructor(context: Context) :
TextToSpeech.OnInitListener {
companion object {
@Volatile
private var instance: TTSManager? = null
fun getInstance(context: Context): TTSManager {
return instance ?: synchronized(this) {
instance ?: TTSManager(context.applicationContext).also {
instance = it
}
}
}
}
private var tts: TextToSpeech? = null
private var isInitialized = false
private val pendingQueue = LinkedList<String>()
private var currentLanguage = Locale.CHINESE
init {
initTTS(context)
}
private fun initTTS(context: Context) {
tts = TextToSpeech(context, this).apply {
setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {
// 开始朗读
}
override fun onDone(utteranceId: String?) {
// 朗读完成
}
override fun onError(utteranceId: String?) {
// 发生错误
}
})
}
}
override fun onInit(status: Int) {
isInitialized = if (status == TextToSpeech.SUCCESS) {
setLanguage(currentLanguage)
true
} else {
false
}
if (isInitialized) {
processPendingQueue()
}
}
fun speak(text: String, flush: Boolean = true) {
if (!isInitialized || tts == null) {
pendingQueue.add(text)
return
}
val utteranceId = System.currentTimeMillis().toString()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val params = Bundle().apply {
putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId)
}
tts?.speak(text,
if (flush) TextToSpeech.QUEUE_FLUSH else TextToSpeech.QUEUE_ADD,
params,
utteranceId)
} else {
@Suppress("DEPRECATION")
val params = HashMap<String, String>()
params[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = utteranceId
tts?.speak(text,
if (flush) TextToSpeech.QUEUE_FLUSH else TextToSpeech.QUEUE_ADD,
params)
}
}
fun setLanguage(locale: Locale): Boolean {
currentLanguage = locale
return if (isInitialized) {
tts?.setLanguage(locale) == TextToSpeech.LANG_COUNTRY_AVAILABLE
} else {
false
}
}
fun stop() {
tts?.stop()
}
fun pause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tts?.stop()
}
}
fun resume() {
// 根据需要实现
}
fun setSpeechRate(rate: Float) {
tts?.setSpeechRate(rate)
}
fun setPitch(pitch: Float) {
tts?.setPitch(pitch)
}
private fun processPendingQueue() {
while (pendingQueue.isNotEmpty()) {
speak(pendingQueue.poll())
}
}
fun release() {
tts?.stop()
tts?.shutdown()
tts = null
isInitialized = false
instance = null
}
}

View File

@@ -1,82 +0,0 @@
package com.xscm.moduleutil.utils;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import com.lahm.library.EasyProtectorLib;
import com.lahm.library.EmulatorCheckCallback;
/**
* ProjectName: BubbleVoice
* Package: com.lnkj.lib_utils
* Description: java类作用描述
* Author: 姚闻达
* CreateDate: 2020/12/15 17:27
* UpdateUser: 更新者
* UpdateDate: 2020/12/15 17:27
* UpdateRemark: 更新说明
* Version: 1.0
*/
public class UtilConfig {
private static Context context;
public static String emulator = "0";
public static String salt = "";
public static void init(Context context) {
setContext(context);
}
public static Context getContext() {
return context;
}
public static void setContext(Context context) {
UtilConfig.context = context;
// checkInEmulator();
}
public static void checkInEmulator() {
emulator = EasyProtectorLib.checkIsRunningInEmulator(getContext(), new EmulatorCheckCallback() {
@Override
public void findEmulator(String emulatorInfo) {
// Logger.e(emulatorInfo);
}
}) ? "1" : "0";
}
public static String getSalt() {
return salt;
}
public static void setSalt(String salt) {
UtilConfig.salt = salt;
}
public static int getAttBg(Context context, int attr){
// 1. 定义需要获取的自定义属性数组
int[] attrs = new int[]{attr};
// 2. 从Context的Theme中获取TypedArray核心绑定当前主题的属性值
TypedArray ta = context.obtainStyledAttributes(attrs);
// 3. 获取attr对应的资源ID默认值0无有效资源时返回0
int bgResId = ta.getResourceId(0, 0);
// 4. 关键手动回收TypedArray释放系统资源避免内存泄漏
ta.recycle();
// 5. 有有效资源ID时设置背景
return bgResId;
}
// 新增:解析自定义颜色属性,返回实际颜色码(核心)
public static int getAttrColor(Context context, int colorAttrId) {
if (context == null) {
return Color.BLACK; // 上下文为空时返回默认黑色,可自定义
}
// 从主题中获取TypedArray解析颜色属性
TypedArray ta = context.obtainStyledAttributes(new int[]{colorAttrId});
// getColor(索引, 默认颜色)直接返回十六进制颜色码适配setTextColor
int color = ta.getColor(0, Color.BLACK);
ta.recycle(); // 必须回收,避免内存泄漏
return color;
}
}

View File

@@ -1,57 +0,0 @@
package com.voice.lib_base.ext
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.ParameterizedType
/**
* 作者 : QIngNing
* 时间 : 2021/12/21
* 描述 :
*/
@JvmName("inflateWithGeneric")
fun <VB : ViewBinding> AppCompatActivity.inflateBindingWithGeneric(layoutInflater: LayoutInflater): VB =
withGenericBindingClass<VB>(this) { clazz ->
clazz.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as VB
}.also { binding ->
if (binding is ViewDataBinding) {
binding.lifecycleOwner = this
}
}
@JvmName("inflateWithGeneric")
fun <VB : ViewBinding> Fragment.inflateBindingWithGeneric(layoutInflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean): VB =
withGenericBindingClass<VB>(this) { clazz ->
clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
.invoke(null, layoutInflater, parent, attachToParent) as VB
}.also { binding ->
if (binding is ViewDataBinding) {
binding.lifecycleOwner = viewLifecycleOwner
}
}
private fun <VB : ViewBinding> withGenericBindingClass(any: Any, block: (Class<VB>) -> VB): VB {
var genericSuperclass = any.javaClass.genericSuperclass
var superclass = any.javaClass.superclass
while (superclass != null) {
if (genericSuperclass is ParameterizedType) {
try {
return block.invoke(genericSuperclass.actualTypeArguments[0] as Class<VB>)
} catch (e: NoSuchMethodException) {
} catch (e: ClassCastException) {
} catch (e: InvocationTargetException) {
throw e.targetException
}
}
genericSuperclass = superclass.genericSuperclass
superclass = superclass.superclass
}
throw IllegalArgumentException("There is no generic of ViewBinding.")
}

View File

@@ -1,971 +0,0 @@
package com.xscm.moduleutil.utils.roomview;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.Interpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.blankj.utilcode.util.LogUtils;
import com.tencent.qgame.animplayer.AnimConfig;
import com.tencent.qgame.animplayer.AnimView;
import com.tencent.qgame.animplayer.inter.IAnimListener;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.utils.ImageUtils;
import com.xscm.moduleutil.widget.CircularImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* 项目名称:羽声语音
* 时间2025/11/24 9:50
* 用途:
*/
public class RoomCPView extends FrameLayout {
private AnimView anim_cp;
private boolean isLoadEffect = false;
private boolean isShow = true; // 是否开启特效
private ReentrantLock lock = new ReentrantLock();
private List<String> animationArray = new ArrayList<>();
public ExecutorService queue = Executors.newSingleThreadExecutor();
private Context mContext;
private boolean isOnece;
private CircularImage room_cp_head1;
private CircularImage room_cp_head2;
private TextView room_cp_name1, room_cp_name2;
private LinearLayout avatarContainer1;
private ConstraintLayout avatarsParentContainer;
// 动画队列和锁机制
private final Queue<AnimationTask> animationQueue = new LinkedList<>();
private boolean isAnimationRunning = false;
private final Object animationLock = new Object();
// 下载任务缓存
private final Queue<DownloadTask> downloadQueue = new LinkedList<>();
private boolean isDownloadRunning = false;
private final Object downloadLock = new Object();
private String currPlayPath = "";
public void setQueue(ExecutorService queue) {
this.queue = queue;
}
public RoomCPView(@NonNull Context context) {
super(context);
this.mContext = context;
init();
}
public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
init();
}
public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init();
}
public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
init();
}
private void init() {
isLoadEffect = false;
initView();
// 设置动画监听器
setupAnimListener();
}
/**
* 设置动画监听器
*/
private void setupAnimListener() {
if (anim_cp != null) {
anim_cp.setAnimListener(new IAnimListener() {
@Override
public void onVideoDestroy() {
LogUtils.e("onVideoDestroy");
}
@Override
public void onVideoComplete() {
LogUtils.e("onVideoComplete");
// 确保所有UI操作在主线程中执行
post(() -> {
if (anim_cp != null) {
// 停止头像动画
stopAvatarAnimation();
// 隐藏动画视图和头像
anim_cp.setVisibility(View.GONE);
avatarContainer1.setVisibility(View.GONE);
if (isOnece) {
return;
}
// 通知播放完成
notifyPlaybackComplete();
loadStartAnimation();
}
});
}
@Override
public void onVideoRender(int i, @Nullable AnimConfig animConfig) {
// LogUtils.e("onVideoRender", i, animConfig);
}
@Override
public void onVideoStart() {
LogUtils.e("onVideoStart");
// 确保所有UI操作在主线程中执行
post(() -> {
// 动画开始,显示视图和头像
setVisibility(View.VISIBLE);
anim_cp.setVisibility(View.VISIBLE);
avatarContainer1.setVisibility(View.VISIBLE);
// 启动头像上下浮动动画
startAvatarFloatAnimation();
});
}
@Override
public boolean onVideoConfigReady(@NonNull AnimConfig animConfig) {
LogUtils.e("onVideoConfigReady", animConfig);
return true;
}
@Override
public void onFailed(int i, @Nullable String s) {
LogUtils.e("onFailed", s);
// 确保所有UI操作在主线程中执行
post(() -> {
if (anim_cp != null) {
// 停止头像动画
stopAvatarAnimation();
// 隐藏动画视图和头像
anim_cp.setVisibility(View.GONE);
avatarContainer1.setVisibility(View.GONE);
if (isOnece) {
return;
}
// 通知播放完成
notifyPlaybackComplete();
loadStartAnimation();
}
});
}
});
}
}
private void initView() {
LayoutInflater.from(getContext()).inflate(R.layout.room_cp_vip_view, this, true);
anim_cp = findViewById(R.id.anim_cp);
room_cp_head1 = findViewById(R.id.room_cp_head1);
room_cp_head2 = findViewById(R.id.room_cp_head2);
room_cp_name1 = findViewById(R.id.room_cp_name1);
room_cp_name2 = findViewById(R.id.room_cp_name2);
// 获取头像的父布局
avatarContainer1 = findViewById(R.id.ll_head);
// 获取包含两个头像的父 LinearLayout
avatarsParentContainer = (ConstraintLayout) avatarContainer1.getParent();
// // 初始状态隐藏头像和名称
// room_cp_head1.setVisibility(View.GONE);
// room_cp_head2.setVisibility(View.GONE);
// room_cp_name1.setVisibility(View.GONE);
// room_cp_name2.setVisibility(View.GONE);
}
public void setCPTextData(String room_head1, String room_head2, String room_cp_name1, String room_cp_name2) {
ImageUtils.loadHeadCC(room_head1, room_cp_head1);
ImageUtils.loadHeadCC(room_head2, room_cp_head2);
this.room_cp_name1.setText(room_cp_name1);
this.room_cp_name2.setText(room_cp_name2);
}
private void loadStartAnimation() {
if (!isShow) {
// isshow 为是否开启特效 如果未开启则return
return;
}
String animationPath = null;
// list加锁
lock.lock();
try {
// 如果list长度大于0
if (!animationArray.isEmpty()) {
// 动画路径则赋值取list中的第一个数据
animationPath = animationArray.get(0);
// 移除list的第一条数据
animationArray.remove(0);
isLoadEffect = true;
} else {
isLoadEffect = false;
// 队列为空,释放资源但不销毁视图
post(() -> {
destroyEffectView();
});
}
} finally {
// 解锁
lock.unlock();
}
if (isLoadEffect && animationPath != null && !TextUtils.isEmpty(animationPath)) {
String finalAnimationPath = animationPath;
post(new Runnable() {
@Override
public void run() {
// 处理MP4动画文件可能是网络URL
handleMP4File(finalAnimationPath, new DownloadCallback() {
@Override
public void onSuccess(String localPath) {
post(() -> {
// 设置MP4动画文件
currPlayPath = localPath;
// 开始播放动画
anim_cp.setLoop(1);
startBottomUpAnimation();
setVisibility(View.VISIBLE);
anim_cp.setVisibility(View.VISIBLE);
avatarContainer1.setVisibility(VISIBLE);
anim_cp.startPlay(new File(currPlayPath));
});
}
@Override
public void onError(String error) {
LogUtils.e("MP4下载或播放失败: " + error);
// 处理失败情况,继续播放下一个
post(() -> {
lock.lock();
try {
isLoadEffect = false;
} finally {
lock.unlock();
}
loadStartAnimation();
});
}
});
}
});
}
}
/**
* CP特效进来
*
* @param room_head1 头像1
* @param room_head2 头像2
* @param room_name1 名称1
* @param room_name2 名称2
* @param mp4Path MP4动画文件路径
*/
public void displayEffectView(String room_head1, String room_head2, String room_name1, String room_name2, String mp4Path) {
// 确保视图已初始化
reinitView();
// 设置CP数据但不显示头像
setCPTextData(room_head1, room_head2, room_name1, room_name2);
// 确保头像初始为隐藏状态
// room_cp_head1.setVisibility(View.GONE);
// room_cp_head2.setVisibility(View.GONE);
// room_cp_name1.setVisibility(View.GONE);
// room_cp_name2.setVisibility(View.GONE);
// 确保视图可见
setVisibility(View.VISIBLE);
// 检查队列是否已初始化
if (queue == null) {
queue = Executors.newSingleThreadExecutor();
}
queue.execute(new Runnable() {
@SuppressLint("CheckResult")
@Override
public void run() {
// 如果mp4Path不存在return
if (mp4Path == null || mp4Path.isEmpty()) {
return;
}
// 将mp4Path链接转为小写
String playImage = mp4Path;
String pathExtension = getFileExtension(playImage).toLowerCase();
// 判定礼物的后缀是否为svga或者mp4如果非这两种 则return
if (!("svga".equals(pathExtension) || "mp4".equals(pathExtension))) {
return;
}
// 锁住list
lock.lock();
try {
// 添加动画数据进list
animationArray.add(playImage);
// 如果没有在加载则开始加载
if (!isLoadEffect) {
// 更改加载状态标记
Observable.timer(2000, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
isLoadEffect = true;
loadStartAnimation();
}
});
}
} finally {
// 解锁
lock.unlock();
}
}
});
}
/**
* 原始的动画方法,现在只被内部调用
*/
public void startAnimation(AnimationListener listener) {
// 设置进入动画
AlphaAnimation fadeIn = new AlphaAnimation(0f, 1f);
fadeIn.setDuration(500);
fadeIn.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
// 保持显示1秒后开始退出动画
postDelayed(() -> {
AlphaAnimation fadeOut = new AlphaAnimation(1f, 0f);
fadeOut.setDuration(500);
fadeOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (listener != null) {
listener.onAnimationEnd();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
startAnimation(fadeOut);
}, 1000);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
startAnimation(fadeIn);
}
public interface AnimationListener {
void onAnimationEnd();
}
// 添加播放完成监听接口
public interface OnPlaybackCompleteListener {
void onPlaybackComplete();
}
// 添加成员变量
private List<OnPlaybackCompleteListener> playbackCompleteListeners = new ArrayList<>();
// 添加监听器管理方法
public void addOnPlaybackCompleteListener(OnPlaybackCompleteListener listener) {
if (listener != null && !playbackCompleteListeners.contains(listener)) {
playbackCompleteListeners.add(listener);
}
}
public void removeOnPlaybackCompleteListener(OnPlaybackCompleteListener listener) {
if (listener != null) {
playbackCompleteListeners.remove(listener);
}
}
public void clearPlaybackCompleteListeners() {
playbackCompleteListeners.clear();
}
// 触发播放完成回调的方法
private void notifyPlaybackComplete() {
for (OnPlaybackCompleteListener listener : new ArrayList<>(playbackCompleteListeners)) {
if (listener != null) {
listener.onPlaybackComplete();
}
}
}
/**
* 动画任务类,用于存储动画所需的数据
*/
public static class AnimationTask {
String room_head1;
String room_head2;
String room_cp_name1;
String room_cp_name2;
String mp4Path; // MP4动画文件路径
AnimationListener listener;
AnimationTask(String room_head1, String room_head2, String room_cp_name1, String room_cp_name2, String mp4Path, AnimationListener listener) {
this.room_head1 = room_head1;
this.room_head2 = room_head2;
this.room_cp_name1 = room_cp_name1;
this.room_cp_name2 = room_cp_name2;
this.mp4Path = mp4Path;
this.listener = listener;
}
}
/**
* 处理MP4文件如果是网络URL则下载到本地
*
* @param path 文件路径或URL
* @param callback 下载回调
*/
private void handleMP4File(String path, DownloadCallback callback) {
// 判断是否是网络URL
if (path != null && (path.startsWith("http://") || path.startsWith("https://"))) {
// 是网络URL需要下载
downloadFile(path, callback);
} else {
// 本地路径,直接使用
if (callback != null) {
callback.onSuccess(path);
}
}
}
/**
* 下载文件到本地
*
* @param url 下载URL
* @param callback 下载回调
*/
private void downloadFile(String url, DownloadCallback callback) {
synchronized (downloadLock) {
// 创建下载任务并加入队列
DownloadTask task = new DownloadTask(url, callback);
downloadQueue.offer(task);
// 如果当前没有下载任务在执行,则开始下载
if (!isDownloadRunning) {
processNextDownload();
}
}
}
/**
* 处理下一个下载任务
*/
private void processNextDownload() {
synchronized (downloadLock) {
if (downloadQueue.isEmpty()) {
isDownloadRunning = false;
return;
}
isDownloadRunning = true;
DownloadTask currentTask = downloadQueue.poll();
// 执行下载任务
new DownloadAsyncTask(currentTask.callback).execute(currentTask.url);
}
}
/**
* 异步下载任务
*/
private class DownloadAsyncTask extends AsyncTask<String, Void, String> {
private DownloadCallback callback;
private String errorMessage;
public DownloadAsyncTask(DownloadCallback callback) {
this.callback = callback;
}
@Override
protected String doInBackground(String... urls) {
String url = urls[0];
try {
// 获取文件名
int lastSlashIndex = url.lastIndexOf('/');
String fileName = lastSlashIndex != -1 ? url.substring(lastSlashIndex + 1) : url;
int queryIndex = fileName.indexOf("?");
if (queryIndex != -1) {
fileName = fileName.substring(0, queryIndex);
}
// 创建本地文件
File dir = new File(getContext().getCacheDir(), "animations");
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, fileName);
// 如果文件已存在,直接返回本地路径
if (file.exists()) {
return file.getAbsolutePath();
}
// 下载文件
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
errorMessage = "服务器返回HTTP错误代码: " + connection.getResponseCode();
return null;
}
InputStream input = connection.getInputStream();
FileOutputStream output = new FileOutputStream(file);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
output.close();
input.close();
connection.disconnect();
return file.getAbsolutePath();
} catch (IOException e) {
errorMessage = "下载失败: " + e.getMessage();
return null;
}
}
@Override
protected void onPostExecute(String result) {
synchronized (downloadLock) {
// 下载完成,处理下一个下载任务
processNextDownload();
}
// 通知回调
if (callback != null) {
if (result != null) {
callback.onSuccess(result);
} else {
callback.onError(errorMessage != null ? errorMessage : "未知错误");
}
}
}
}
/**
* 下载任务类
*/
private static class DownloadTask {
String url;
DownloadCallback callback;
DownloadTask(String url, DownloadCallback callback) {
this.url = url;
this.callback = callback;
}
}
/**
* 下载回调接口
*/
private interface DownloadCallback {
void onSuccess(String localPath);
void onError(String error);
}
/**
* 获取文件扩展名
*
* @param url 文件URL
* @return 文件扩展名
*/
private String getFileExtension(String url) {
if (url != null && url.contains(".")) {
return url.substring(url.lastIndexOf(".") + 1);
}
return "";
}
/**
* 销毁特效视图
*/
public void destroyEffectView() {
// 停止头像动画
stopAvatarAnimation();
// 清理监听器
clearPlaybackCompleteListeners();
// 停止动画视图但保留组件以便重用
if (anim_cp != null) {
anim_cp.stopPlay();
anim_cp.setVisibility(View.GONE);
}
// 隐藏视图但不移除,以便再次使用
setVisibility(View.GONE);
// 清空动画队列
lock.lock();
try {
animationArray.clear();
isLoadEffect = false;
} finally {
lock.unlock();
}
}
/**
* 从底部弹起动画
*/
private void startBottomUpAnimation() {
// 获取父视图的高度如果为0则使用屏幕高度
float parentHeight = 0;
if (getParent() != null) {
parentHeight = ((View) getParent()).getHeight();
}
if (parentHeight <= 0) {
parentHeight = getResources().getDisplayMetrics().heightPixels;
}
// 设置初始位置在屏幕底部外
// setTranslationY(parentHeight);
// 创建从底部弹起的动画
TranslateAnimation bottomUpAnimation = new TranslateAnimation(
Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 0,
Animation.RELATIVE_TO_PARENT, 1.0f, Animation.RELATIVE_TO_PARENT, 0.0f);
bottomUpAnimation.setDuration(2000); // 动画持续时间
// 创建弹性插值器
Interpolator overshootInterpolator = new OvershootInterpolator(0.5f);
bottomUpAnimation.setInterpolator(overshootInterpolator);
// 应用动画
startAnimation(bottomUpAnimation);
// 动画结束后重置位置
bottomUpAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
// setVisibility(View.VISIBLE);
// anim_cp.setVisibility(View.VISIBLE);
// avatarContainer1.setVisibility(VISIBLE);
// anim_cp.startPlay(new File(currPlayPath));
}
@Override
public void onAnimationEnd(Animation animation) {
startAvatarFloatAnimation();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
avatarsParentContainer.startAnimation(bottomUpAnimation);
}
/**
* 头像上下动画
*/
private void startAvatarFloatAnimation() {
// 确保包含两个头像的父布局已初始化
if (avatarsParentContainer == null) {
return;
}
// 包含两个头像的父布局上下浮动动画
AnimationSet avatarsAnimationSet = new AnimationSet(true);
// 使用适中的移动距离,确保头像框显示完整
TranslateAnimation avatarsFloatUp = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f,
Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, -0.01f);
avatarsFloatUp.setDuration(900); // 增加动画持续时间,使动画更平滑
avatarsFloatUp.setRepeatCount(10);
avatarsFloatUp.setRepeatMode(Animation.REVERSE);
avatarsAnimationSet.addAnimation(avatarsFloatUp);
avatarsParentContainer.startAnimation(avatarsAnimationSet);
// 动画结束后重置位置
avatarsAnimationSet.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (avatarContainer1.getVisibility() == View.VISIBLE) {
avatarContainer1.setVisibility(View.GONE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
/**
* 停止头像动画
*/
private void stopAvatarAnimation() {
if (avatarsParentContainer != null) {
avatarsParentContainer.clearAnimation();
}
}
/**
* 停止播放动画
*/
public void stopPlay() {
stopAvatarAnimation();
if (anim_cp != null) {
anim_cp.stopPlay();
anim_cp.setVisibility(View.GONE);
}
setVisibility(View.GONE);
}
/**
* 开启或关闭特效视图
*
* @param isShow 是否显示
*/
public void openOrCloseEffectViewWith(boolean isShow) {
this.isShow = isShow;
if (anim_cp != null) {
anim_cp.stopPlay();
anim_cp.setVisibility(View.GONE);
}
setVisibility(isShow ? View.VISIBLE : View.GONE);
}
/**
* 下载并播放动画
*
* @param context 上下文
* @param playImage 动画URL
* @param callback 下载回调
*/
public void downloadAndPlay(Context context, String playImage, DownloadCallback callback) {
String fileName = playImage.substring(playImage.lastIndexOf("/"));
String filePath = context.getCacheDir().getAbsolutePath() + fileName;
LogUtils.e("@@@@@filePath: " + filePath.toString());
File file = new File(filePath);
if (!file.exists()) {
LogUtils.e("无缓存");
// 使用OkHttp进行下载
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(playImage)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
LogUtils.e("MP4下载失败: " + e.toString());
// 在主线程中回调失败
post(() -> {
if (callback != null) {
callback.onError(e.getMessage());
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
try (ResponseBody responseBody = response.body()) {
if (responseBody != null) {
File downloadedFile = new File(filePath);
FileOutputStream fos = new FileOutputStream(downloadedFile);
fos.write(responseBody.bytes());
fos.close();
// 在主线程中回调成功
post(() -> {
if (callback != null) {
callback.onSuccess(downloadedFile.getAbsolutePath());
}
});
} else {
// 在主线程中回调失败
post(() -> {
if (callback != null) {
callback.onError("Response body is null");
}
});
}
} catch (Exception e) {
LogUtils.e("MP4文件保存失败: " + e.getMessage());
// 在主线程中回调失败
post(() -> {
if (callback != null) {
callback.onError(e.getMessage());
}
});
}
} else {
LogUtils.e("MP4下载响应失败");
// 在主线程中回调失败
post(() -> {
if (callback != null) {
callback.onError("Response not successful: " + response.code());
}
});
}
}
});
} else {
// 文件已存在,直接回调成功
if (callback != null) {
callback.onSuccess(file.getAbsolutePath());
}
}
}
/**
* 重新初始化视图,以便再次播放
*/
public void reinitView() {
if (anim_cp == null) {
// 如果动画视图已被销毁,重新初始化
initView();
// 重新设置动画监听器
setupAnimListener();
}
// 确保线程池可用
if (queue == null || queue.isShutdown()) {
queue = Executors.newSingleThreadExecutor();
}
// 重置状态
isLoadEffect = false;
isShow = true;
}
/**
* 完全销毁视图,释放所有资源
* 当不再需要此视图时调用
*/
public void completeDestroy() {
// 停止头像动画
stopAvatarAnimation();
// 清理监听器
clearPlaybackCompleteListeners();
// 停止并移除动画视图
if (anim_cp != null) {
anim_cp.stopPlay();
removeView(anim_cp);
anim_cp = null;
}
// 停止线程池
if (queue != null) {
queue.shutdown();
queue = null;
}
// 隐藏并移除整个视图
setVisibility(View.GONE);
if (getParent() != null) {
((ViewGroup) getParent()).removeView(this);
}
}
}

View File

@@ -1,131 +0,0 @@
package com.xscm.moduleutil.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import com.xscm.moduleutil.R;
/**
* 项目名称:羽声语音
* 时间2026/1/4 16:09
* 用途:自定义组件:购买数量,带减少增加按钮
*/
public class AmountView extends LinearLayout implements View.OnClickListener, TextWatcher {
private static final String TAG = "AmountView";
private int amount = 1; //购买数量
private int goods_storage = Integer.MAX_VALUE; //商品库存
private OnAmountChangeListener mListener;
private EditText etAmount;
private Button btnDecrease;
private Button btnIncrease;
public AmountView(Context context) {
this(context, null);
}
public AmountView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.view_amount, this);
etAmount = (EditText) findViewById(R.id.etAmount);
btnDecrease = (Button) findViewById(R.id.btnDecrease);
btnIncrease = (Button) findViewById(R.id.btnIncrease);
btnDecrease.setOnClickListener(this);
btnIncrease.setOnClickListener(this);
etAmount.addTextChangedListener(this);
TypedArray obtainStyledAttributes = getContext().obtainStyledAttributes(attrs, R.styleable.AmountView);
int btnWidth = obtainStyledAttributes.getDimensionPixelSize(R.styleable.AmountView_btnWidth, LayoutParams.WRAP_CONTENT);
int tvWidth = obtainStyledAttributes.getDimensionPixelSize(R.styleable.AmountView_tvWidth, 80);
int tvTextSize = obtainStyledAttributes.getDimensionPixelSize(R.styleable.AmountView_tvTextSize, 0);
int btnTextSize = obtainStyledAttributes.getDimensionPixelSize(R.styleable.AmountView_btnTextSize, 0);
obtainStyledAttributes.recycle();
LayoutParams btnParams = new LayoutParams(btnWidth, LayoutParams.MATCH_PARENT);
btnDecrease.setLayoutParams(btnParams);
btnIncrease.setLayoutParams(btnParams);
if (btnTextSize != 0) {
btnDecrease.setTextSize(TypedValue.COMPLEX_UNIT_PX, btnTextSize);
btnIncrease.setTextSize(TypedValue.COMPLEX_UNIT_PX, btnTextSize);
}
LayoutParams textParams = new LayoutParams(tvWidth, LayoutParams.MATCH_PARENT);
etAmount.setLayoutParams(textParams);
if (tvTextSize != 0) {
etAmount.setTextSize(tvTextSize);
}
}
public void setOnAmountChangeListener(OnAmountChangeListener onAmountChangeListener) {
this.mListener = onAmountChangeListener;
}
public void setGoods_storage(int goods_storage) {
this.goods_storage = goods_storage;
}
@Override
public void onClick(View v) {
int i = v.getId();
if (i == R.id.btnDecrease) {
if (amount > 1) {
amount--;
etAmount.setText(amount + "");
}
} else if (i == R.id.btnIncrease) {
if (amount < goods_storage) {
amount++;
etAmount.setText(amount + "");
}
}
etAmount.clearFocus();
if (mListener != null) {
mListener.onAmountChange(this, amount);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.toString().isEmpty())
return;
amount = Integer.valueOf(s.toString());
if (amount > goods_storage) {
etAmount.setText(goods_storage + "");
return;
}
if (mListener != null) {
mListener.onAmountChange(this, amount);
}
}
public interface OnAmountChangeListener {
void onAmountChange(View view, int amount);
}
}

View File

@@ -1,571 +0,0 @@
package com.xscm.moduleutil.widget;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import com.blankj.utilcode.util.LogUtils;
import com.opensource.svgaplayer.SVGACallback;
import com.opensource.svgaplayer.SVGADrawable;
import com.opensource.svgaplayer.SVGADynamicEntity;
import com.opensource.svgaplayer.SVGAImageView;
import com.opensource.svgaplayer.SVGAParser;
import com.opensource.svgaplayer.SVGAVideoEntity;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.databinding.RoomViewSvgaAnimBinding;
import com.xscm.moduleutil.utils.logger.Logger;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class AvatarFrameView extends FrameLayout {
private boolean isMute = false;
public void setMute(boolean b) {
this.isMute = b;
}
public enum RenderType {SVGA, MP4}
private RenderType renderType;
private SVGAImageView svgaSurface;
private int mType;//1:循环播放 2:播放一次停止播放
private boolean isPlaying = false;
// 添加销毁标记
private boolean isDestroyed = false;
private static final String TAG = "AvatarFrameView";
private RoomViewSvgaAnimBinding mBinding;
private static final int MAX_SVGA_CACHE_SIZE = 3;
private final Map<String, WeakReference<SVGAVideoEntity>> svgaCache = new LruCache<>(MAX_SVGA_CACHE_SIZE);
public AvatarFrameView(@NonNull Context context) {
this(context, null);
}
public AvatarFrameView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AvatarFrameView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.room_view_svga_anim, this, true);
initViews();
}
private void initViews() {
// 初始化 SVGA View
svgaSurface = new SVGAImageView(getContext());
svgaSurface.setVisibility(View.GONE);
addView(svgaSurface);
}
private String getFileExtension(String url) {
if (url == null || url.isEmpty()) return "";
int dotIndex = url.lastIndexOf(".");
if (dotIndex > 0 && dotIndex < url.length() - 1) {
return url.substring(dotIndex + 1).toLowerCase(); // 返回 "mp4", "svga" 等
}
return "";
}
public void setSource(String url, int type2) {
// 添加到播放队列
PlayItem item = new PlayItem(url, type2);
processPlayItem(item);
Logger.d("AvatarFrameView", "Added to queue, queue size: url: " + url);
}
private void processPlayItem(PlayItem item) {
try {
String ext = getFileExtension(item.url);
if ("svga".equalsIgnoreCase(ext)) {
renderType = RenderType.SVGA;
mType = item.type;
if (mBinding != null) {
mBinding.playView.setVisibility(View.GONE);
}
loadSVGA(item.url);
} else if ("mp4".equalsIgnoreCase(ext)) {
renderType = RenderType.MP4;
mType = item.type;
if (mBinding != null) {
mBinding.playView.setVisibility(View.VISIBLE);
downloadAndPlayMp4(item.url);
} else {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(getContext()), R.layout.room_view_svga_anim, this, true);
mBinding.playView.setVisibility(View.VISIBLE);
downloadAndPlayMp4(item.url);
}
}
} catch (Exception e) {
LogUtils.e(TAG, "Error processing play item: " + e.getMessage());
}
}
public boolean isPlaying() {
if (mBinding != null) {
return mBinding.playView.isRunning();
}
return true;
}
boolean isTxk = false;
public void downloadAndPlayMp4(String url) {
// 提取文件名
String fileName = url.substring(url.lastIndexOf("/"));
String filePath = getContext().getCacheDir().getAbsolutePath() + fileName;
LogUtils.e("@@@@@filePath: " + filePath.toString());
File file = new File(filePath);
if (!file.exists()) {
LogUtils.e("无缓存");
// 使用OkHttp进行下载
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
LogUtils.e("MP4下载失败: " + e.toString());
}
// 更简单的优化版本
@Override
public void onResponse(Call call, Response response) throws IOException {
// 在异步回调中首先检查是否已销毁
if (isDestroyed) {
LogUtils.w(TAG, "View destroyed before download completed");
return;
}
LogUtils.d("@@@@", "onResponse" + Thread.currentThread().getName());
if (response.isSuccessful()) {
try (ResponseBody responseBody = response.body()) {
if (responseBody != null) {
// 在后台线程处理文件保存
String fileName = url.substring(url.lastIndexOf("/"));
String filePath = getContext().getCacheDir().getAbsolutePath() + fileName;
File downloadedFile = new File(filePath);
// 使用流式传输避免大文件卡顿
try (InputStream inputStream = responseBody.byteStream();
FileOutputStream fos = new FileOutputStream(downloadedFile)) {
// 定义缓冲区大小8KB
byte[] buffer = new byte[1024 * 1024];
int bytesRead;
// 流式读取并写入文件
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.flush();
isTxk = true;
// 关键在执行UI操作前再次检查是否已销毁
if (downloadedFile.exists()) {
LogUtils.d("@@@@Thread", Thread.currentThread().getName());
playMp4File(downloadedFile); // 使用正确的文件引用
} else {
LogUtils.w(TAG, "View destroyed or file not exist after download");
}
} catch (IOException e) {
LogUtils.e("MP4文件保存失败: " + e.getMessage());
}
}
} catch (Exception e) {
LogUtils.e("MP4文件保存失败: " + e.getMessage());
}
} else {
LogUtils.e("MP4下载响应失败");
}
}
});
} else {
isTxk = true;
// 检查是否已销毁
if (file.exists()) {
LogUtils.e("有缓存:" + file.exists() + "====" + file.getAbsolutePath());
playMp4File(file);
} else {
LogUtils.w(TAG, "有缓存2222222222222");
}
}
}
private void playMp4File(File file) {
try {
// 双重检查确保组件未被销毁
if (isDestroyed) {
LogUtils.w(TAG, "Attempt to play MP4 file after view destroyed");
return;
}
if (mBinding == null) {
LogUtils.w(TAG, "PlayView is null");
return;
}
if (file != null && file.exists()) {
// 设置循环次数根据mType决定
if (mType == 1 || mType == 3) {
mBinding.playView.setLoop(Integer.max(1, 999999999)); // 无限循环
} else {
mBinding.playView.setLoop(1); // 播放一次
}
mBinding.playView.setMute(isMute);
// 开始播放前检查视图状态
if (!isDestroyed && mBinding != null && mBinding.playView != null) {
mBinding.playView.startPlay(file);
} else {
LogUtils.w(TAG, "View was destroyed before MP4 playback started");
}
} else {
LogUtils.e("播放MP4文件出错: 文件不存在或已损坏");
}
} catch (Exception e) {
LogUtils.e("播放MP4文件出错: " + e.getMessage());
}
}
private void handleSVGAComplete(SVGAVideoEntity videoItem, String url) {
if (svgaSurface == null) {
return;
}
try {
// 缓存实体(使用弱引用)
if (svgaCache.size() < MAX_SVGA_CACHE_SIZE) {
svgaCache.put(url, new WeakReference<>(videoItem));
}
SVGADrawable drawable = new SVGADrawable(videoItem, new SVGADynamicEntity());
svgaSurface.setImageDrawable(drawable);
svgaSurface.setCallback(new SVGACallback() {
@Override
public void onStep(int i, double v) {
}
@Override
public void onRepeat() {
// 循环播放处理
if (mType != 1) { // 非循环播放
svgaSurface.stopAnimation();
svgaSurface.clearAnimation();
svgaSurface.setImageDrawable(null);
}
}
@Override
public void onPause() {
}
@Override
public void onFinished() {
if (mType == 1) { // 循环播放
// 继续循环播放
}
}
});
// 设置循环次数
if (mType == 1 || mType == 3) {
svgaSurface.setLoops(0); // 无限循环
} else {
svgaSurface.setLoops(1); // 播放一次
}
svgaSurface.startAnimation();
} catch (Exception e) {
LogUtils.e(TAG, "Error handling SVGA completion: " + e.getMessage());
}
}
public void stopAll() {
if (svgaSurface != null) {
svgaSurface.stopAnimation();
svgaSurface.clearAnimation();
svgaSurface.setImageDrawable(null);
}
// 增加空值检查
if (mBinding != null) {
mBinding.playView.stopPlay();
}
}
public void stopPlay(){
// 增加空值检查
if (mBinding != null && mBinding.playView.isRunning()) {
mBinding.playView.stopPlay();
}
}
private void playCachedSVGA(SVGAVideoEntity videoItem) {
try {
SVGADrawable drawable = new SVGADrawable(videoItem, new SVGADynamicEntity());
svgaSurface.setImageDrawable(drawable);
svgaSurface.setCallback(new SVGACallback() {
@Override
public void onStep(int i, double v) {
}
@Override
public void onRepeat() {
}
@Override
public void onPause() {
}
@Override
public void onFinished() {
isPlaying = false;
}
});
// 设置循环次数
if (mType == 1 || mType == 3) {
svgaSurface.setLoops(0); // 无限循环
} else {
svgaSurface.setLoops(1); // 播放一次
}
svgaSurface.startAnimation();
} catch (Exception e) {
LogUtils.e(TAG, "Error playing cached SVGA: " + e.getMessage());
isPlaying = false;
}
}
private void loadNewSVGA(String url) {
try {
new SVGAParser(getContext()).parse(new URL(url), new SVGAParser.ParseCompletion() {
@Override
public void onComplete(SVGAVideoEntity videoItem) {
if (Looper.myLooper() != Looper.getMainLooper()) {
handleSVGAComplete(videoItem, url);
} else {
handleSVGAComplete(videoItem, url);
}
}
@Override
public void onError() {
isPlaying = false;
}
});
} catch (Exception e) {
LogUtils.e(TAG, "Error parsing SVGA: " + e.getMessage());
isPlaying = false;
}
}
private void loadSVGA(String url) {
try {
svgaSurface.setVisibility(View.VISIBLE);
// 检查缓存
WeakReference<SVGAVideoEntity> cachedRef = svgaCache.get(url);
SVGAVideoEntity cachedEntity = cachedRef != null ? cachedRef.get() : null;
if (cachedEntity != null) {
// 使用缓存的实体
playCachedSVGA(cachedEntity);
return;
} else {
// 加载新的SVGA
loadNewSVGA(url);
}
} catch (Exception e) {
LogUtils.e(TAG, "Error loading SVGA: " + e.getMessage());
isPlaying = false;
}
// svgaSurface.setVisibility(View.VISIBLE);
// try {
// new SVGAParser(getContext()).parse(new URL(url), new SVGAParser.ParseCompletion() {
// @Override
// public void onComplete(SVGAVideoEntity videoItem) {
// SVGADrawable drawable = new SVGADrawable(videoItem, new SVGADynamicEntity());
// svgaSurface.setImageDrawable(drawable);
// svgaSurface.setCallback(new SVGACallback() {
//
// @Override
// public void onStep(int i, double v) {
//
// }
//
// @Override
// public void onRepeat() {
//
// }
//
// @Override
// public void onPause() {
//
// }
//
// @Override
// public void onFinished() {
// isPlaying = false;
// }
// });
//
// svgaSurface.startAnimation();
// }
//
// @Override
// public void onError() {
// isPlaying = false;
// }
// });
// } catch (Exception e) {
// e.printStackTrace();
// }
}
private void clearPrevious() {
try {
// 停止并清理 SVGA 动画
if (svgaSurface != null && svgaSurface.getDrawable() instanceof SVGADrawable) {
SVGADrawable drawable = (SVGADrawable) svgaSurface.getDrawable();
if (drawable != null) {
drawable.stop();
// 清理 SVGADrawable 中的资源
svgaSurface.clearAnimation();
svgaSurface.setImageDrawable(null);
}
}
// 隐藏所有视图
if (svgaSurface != null) svgaSurface.setVisibility(View.GONE);
// 停止播放器
if (mBinding != null && mBinding.playView != null) {
mBinding.playView.stopPlay();
mBinding.playView.setVisibility(View.GONE);
}
} catch (Exception e) {
LogUtils.e(TAG, "Error in clearPrevious: " + e.getMessage());
}
}
// 简单的 LRU Cache 实现
private static class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LruCache(int maxSize) {
super(16, 0.75f, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return size() > maxSize;
}
}
/**
* 释放所有资源
*/
private void releaseResources() {
LogUtils.d(TAG, "Releasing all resources");
performUICleanup();
}
/**
* 在主线程执行 UI 相关的清理操作
*/
private void performUICleanup() {
try {
// 停止并清理播放器
if (mBinding != null) {
try {
mBinding.playView.stopPlay();
} catch (Exception e) {
Logger.e(TAG, "Error stopping playView: " + e.getMessage());
}
}
// 清理 SVGA 资源
if (svgaSurface != null) {
try {
svgaSurface.stopAnimation(true);
svgaSurface.clear();
svgaSurface.clearAnimation();
svgaSurface.setImageDrawable(null);
} catch (Exception e) {
Logger.e(TAG, "Error releasing SVGA resources: " + e.getMessage());
}
}
} catch (Exception e) {
Logger.e(TAG, "Error in performUICleanup: " + e.getMessage());
}
}
/**
* 公共释放方法,用于外部主动释放资源
*/
public void release() {
Logger.d("AvatarFrameView", "Public release called");
isDestroyed = true;
try {
// 清空播放队列
clearQueue();
// 释放所有资源
releaseResources();
} catch (Exception e) {
LogUtils.e(TAG, "Error in AvatarFrameView release: " + e.getMessage());
}
}
public void clearQueue() {
isPlaying = false;
// 清理当前正在播放的内容
clearPrevious();
}
private static class PlayItem {
String url;
int type;
PlayItem(String url, int type) {
this.url = url;
this.type = type;
}
}
}

View File

@@ -1,66 +0,0 @@
package com.xscm.moduleutil.widget
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
/**
* 项目名称:羽声语音
* 时间2025/12/3 9:30
* 用途:
*/
class CustomViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
private var initialX = 0f
private var initialY = 0f
private var isBeingDragged = false
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
// 记录初始触摸点
initialX = ev.x
initialY = ev.y
parent.requestDisallowInterceptTouchEvent(true) // 请求父容器不要拦截事件
}
MotionEvent.ACTION_MOVE -> {
val deltaX = Math.abs(ev.x - initialX)
val deltaY = Math.abs(ev.y - initialY)
// 如果水平滑动距离大于垂直滑动距离才认为是水平滑动ViewPager才拦截事件
if (deltaX > deltaY && deltaX > 30) { // 30是阈值可以根据需要调整
return super.onInterceptTouchEvent(ev)
}
// 如果是向上滑动,确保父容器不拦截
if (deltaY > deltaX && ev.y < initialY) {
parent.requestDisallowInterceptTouchEvent(true)
return false
}
// // 否则,不拦截,让子视图处理
// parent.requestDisallowInterceptTouchEvent(true)
// return false
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isBeingDragged = false
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
if (isBeingDragged) {
parent.requestDisallowInterceptTouchEvent(true)
}
}
}
return super.onTouchEvent(ev)
}
}

View File

@@ -1,465 +0,0 @@
package com.xscm.moduleutil.widget;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TabHost;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.xscm.moduleutil.R;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
/**
* @Author qx
* @Time 2026/1/21 8:59
* @Description 时间选择器
*/
public class DoubleTimePickerBottomSheet extends BottomSheetDialogFragment {
private OnTimeRangeSelectedListener listener;
private boolean showTime = true; // 默认显示时分秒
private boolean showDate = true; // 默认显示日期
private boolean showSeconds = true; // 默认显示秒
// 格式模板
private static final String DATE_FORMAT = "yyyy-MM-dd";
private static final String TIME_FORMAT_SECONDS = "HH:mm:ss";
private static final String TIME_FORMAT_NO_SECONDS = "HH:mm";
private static final String DATETIME_FORMAT_SECONDS = "yyyy-MM-dd HH:mm:ss";
private static final String DATETIME_FORMAT_NO_SECONDS = "yyyy-MM-dd HH:mm";
private SimpleDateFormat dateFormat;
private SimpleDateFormat timeFormat;
private SimpleDateFormat dateTimeFormat;
// UI组件
private TextView tvStartTime;
private TextView tvEndTime;
private TabHost tabHost;
private WheelDatePicker wheelStartDate;
private WheelTimePicker wheelStartTime;
private WheelDatePicker wheelEndDate;
private WheelTimePicker wheelEndTime;
// 时间数据
private Calendar startCalendar;
private Calendar endCalendar;
private View view;
private String start_time;
private String end_time;
public interface OnTimeRangeSelectedListener {
void onTimeRangeSelected(Date startDate, Date endDate);
}
public void setOnTimeRangeSelectedListener(OnTimeRangeSelectedListener listener) {
this.listener = listener;
}
/**
* 开始时间
*
* @param startTime
*/
public void setTvStartTime(String startTime) {
this.start_time = startTime;
}
public void setTvEndTime(String endTime) {
this.end_time = endTime;
}
/**
* 设置是否显示时分秒
*
* @param showTime true显示年月日时分秒false只显示年月日
*/
public void setShowTime(boolean showTime) {
this.showTime = showTime;
}
/**
* 设置是否显示日期
*
* @param showDate true显示日期false只显示时间
*/
public void setShowDate(boolean showDate) {
this.showDate = showDate;
}
/**
* 设置是否显示秒
*
* @param showSeconds true显示秒false不显示秒
*/
public void setShowSeconds(boolean showSeconds) {
this.showSeconds = showSeconds;
}
/**
* 创建实例
*
* @param showTime true显示年月日时分秒false只显示年月日
* @return DoubleTimePickerBottomSheet实例
*/
public static DoubleTimePickerBottomSheet newInstance(boolean showTime) {
DoubleTimePickerBottomSheet fragment = new DoubleTimePickerBottomSheet();
fragment.setShowTime(showTime);
return fragment;
}
/**
* 创建实例
*
* @param showTime true显示年月日时分秒false只显示年月日
* @param showDate true显示日期false只显示时间
* @return DoubleTimePickerBottomSheet实例
*/
public static DoubleTimePickerBottomSheet newInstance(boolean showTime, boolean showDate) {
DoubleTimePickerBottomSheet fragment = new DoubleTimePickerBottomSheet();
fragment.setShowTime(showTime);
fragment.setShowDate(showDate);
return fragment;
}
/**
* 创建实例
*
* @param start_time 开始时间
* @param end_time 结束时间
* @return
*/
public static DoubleTimePickerBottomSheet newInstance(String start_time, String end_time) {
DoubleTimePickerBottomSheet fragment = new DoubleTimePickerBottomSheet();
fragment.setTvStartTime(start_time);
fragment.setTvEndTime(end_time);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new CustomBottomSheetDialog(requireContext());
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化格式化工具
initFormats();
// 初始化时间数据
initCalendars();
}
/**
* 初始化日期时间格式化工具
*/
private void initFormats() {
dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.getDefault());
if (showSeconds) {
timeFormat = new SimpleDateFormat(TIME_FORMAT_SECONDS, Locale.getDefault());
dateTimeFormat = new SimpleDateFormat(DATETIME_FORMAT_SECONDS, Locale.getDefault());
} else {
timeFormat = new SimpleDateFormat(TIME_FORMAT_NO_SECONDS, Locale.getDefault());
dateTimeFormat = new SimpleDateFormat(DATETIME_FORMAT_NO_SECONDS, Locale.getDefault());
}
}
/**
* 初始化时间数据
*/
private void initCalendars() {
startCalendar = Calendar.getInstance();
endCalendar = Calendar.getInstance();
// 设置默认结束时间略晚于开始时间
if (showTime) {
endCalendar.add(Calendar.HOUR, 1);
} else {
endCalendar.add(Calendar.DAY_OF_MONTH, 1);
}
}
/**
* 根据配置获取显示格式
*/
private String getFormattedTime(Calendar calendar) {
if (showDate && showTime) {
return dateTimeFormat.format(calendar.getTime());
} else if (showDate && !showTime) {
return dateFormat.format(calendar.getTime());
} else if (!showDate && showTime) {
return timeFormat.format(calendar.getTime());
}
return "";
}
/**
* 更新顶部TextView显示
*/
private void updateDisplayText() {
if (tvStartTime != null) {
tvStartTime.setText(getFormattedTime(startCalendar));
}
if (tvEndTime != null) {
tvEndTime.setText(getFormattedTime(endCalendar));
}
}
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view = inflater.inflate(R.layout.dialog_double_time_picker, container, false);
initViews(view);
setupWheelPickers();
setupTabHost();
setupButtons();
return view;
}
/**
* 初始化视图组件
*/
private void initViews(View view) {
tvStartTime = view.findViewById(R.id.tv_start);
tvEndTime = view.findViewById(R.id.tv_end);
tabHost = view.findViewById(R.id.tabHost);
wheelStartDate = view.findViewById(R.id.wheel_start_date);
wheelStartTime = view.findViewById(R.id.wheel_start_time);
wheelEndDate = view.findViewById(R.id.wheel_end_date);
wheelEndTime = view.findViewById(R.id.wheel_end_time);
// 初始化显示文本
updateDisplayText();
tvStartTime.setText(start_time);
tvEndTime.setText(end_time);
}
/**
* 设置滚轮选择器
*/
private void setupWheelPickers() {
// 根据showDate和showTime参数控制日期和时间选择器的显示和布局
adjustPickerVisibility();
// 初始化开始时间
if (showDate) {
wheelStartDate.init(startCalendar.get(Calendar.YEAR),
startCalendar.get(Calendar.MONTH),
startCalendar.get(Calendar.DAY_OF_MONTH));
wheelStartDate.setOnDateSelectedListener((year, month, day) -> {
startCalendar.set(year, month, day);
updateDisplayText();
});
}
if (showTime) {
wheelStartTime.init(startCalendar.get(Calendar.HOUR_OF_DAY),
startCalendar.get(Calendar.MINUTE),
showSeconds ? startCalendar.get(Calendar.SECOND) : 0);
wheelStartTime.setShowSeconds(showSeconds);
wheelStartTime.setOnValueChangedListener(() -> {
startCalendar.set(Calendar.HOUR_OF_DAY, wheelStartTime.getHour());
startCalendar.set(Calendar.MINUTE, wheelStartTime.getMinute());
if (showSeconds) {
startCalendar.set(Calendar.SECOND, wheelStartTime.getSecond());
}
updateDisplayText();
});
}
// 初始化结束时间
if (showDate) {
wheelEndDate.init(endCalendar.get(Calendar.YEAR),
endCalendar.get(Calendar.MONTH),
endCalendar.get(Calendar.DAY_OF_MONTH));
wheelEndDate.setOnDateSelectedListener((year, month, day) -> {
endCalendar.set(year, month, day);
updateDisplayText();
});
}
if (showTime) {
wheelEndTime.init(endCalendar.get(Calendar.HOUR_OF_DAY),
endCalendar.get(Calendar.MINUTE),
showSeconds ? endCalendar.get(Calendar.SECOND) : 0);
wheelEndTime.setShowSeconds(showSeconds);
wheelEndTime.setOnValueChangedListener(() -> {
endCalendar.set(Calendar.HOUR_OF_DAY, wheelEndTime.getHour());
endCalendar.set(Calendar.MINUTE, wheelEndTime.getMinute());
if (showSeconds) {
endCalendar.set(Calendar.SECOND, wheelEndTime.getSecond());
}
updateDisplayText();
});
}
}
/**
* 调整选择器显示状态和布局
*/
private void adjustPickerVisibility() {
if (showDate && showTime) {
// 显示日期和时间时,使用默认布局
setPickerVisibility(View.VISIBLE, View.VISIBLE, View.VISIBLE, View.VISIBLE);
resetPickerLayoutParams(wheelStartDate, wheelEndDate);
} else if (showDate && !showTime) {
// 只显示日期时,隐藏时间选择器并让日期选择器铺满宽度
setPickerVisibility(View.VISIBLE, View.GONE, View.VISIBLE, View.GONE);
setPickerFullWidth(wheelStartDate, wheelEndDate);
} else if (!showDate && showTime) {
// 只显示时间时,隐藏日期选择器并让时间选择器铺满宽度
setPickerVisibility(View.GONE, View.VISIBLE, View.GONE, View.VISIBLE);
setPickerFullWidth(wheelStartTime, wheelEndTime);
}
}
private void setPickerVisibility(int startDateVis, int startTimeVis,
int endDateVis, int endTimeVis) {
wheelStartDate.setVisibility(startDateVis);
wheelStartTime.setVisibility(startTimeVis);
wheelEndDate.setVisibility(endDateVis);
wheelEndTime.setVisibility(endTimeVis);
}
private void resetPickerLayoutParams(WheelDatePicker... pickers) {
for (WheelDatePicker picker : pickers) {
ViewGroup.LayoutParams params = picker.getLayoutParams();
params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
picker.setLayoutParams(params);
}
}
private void setPickerFullWidth(View... views) {
for (View view : views) {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
view.setLayoutParams(params);
}
}
/**
* 设置TabHost
*/
private void setupTabHost() {
tabHost.setup();
tabHost.addTab(tabHost.newTabSpec("start").setIndicator("开始时间").setContent(R.id.start_time_tab));
tabHost.addTab(tabHost.newTabSpec("end").setIndicator("结束时间").setContent(R.id.end_time_tab));
tabHost.setOnTabChangedListener(tabId -> {
if ("start".equals(tabId)) {
restoreStartTimeSelection();
} else if ("end".equals(tabId)) {
restoreEndTimeSelection();
}
});
}
/**
* 恢复开始时间选择
*/
private void restoreStartTimeSelection() {
if (showDate) {
wheelStartDate.init(startCalendar.get(Calendar.YEAR),
startCalendar.get(Calendar.MONTH),
startCalendar.get(Calendar.DAY_OF_MONTH));
}
if (showTime) {
wheelStartTime.init(startCalendar.get(Calendar.HOUR_OF_DAY),
startCalendar.get(Calendar.MINUTE),
showSeconds ? startCalendar.get(Calendar.SECOND) : 0);
}
}
/**
* 恢复结束时间选择
*/
private void restoreEndTimeSelection() {
if (showDate) {
wheelEndDate.init(endCalendar.get(Calendar.YEAR),
endCalendar.get(Calendar.MONTH),
endCalendar.get(Calendar.DAY_OF_MONTH));
}
if (showTime) {
wheelEndTime.init(endCalendar.get(Calendar.HOUR_OF_DAY),
endCalendar.get(Calendar.MINUTE),
showSeconds ? endCalendar.get(Calendar.SECOND) : 0);
}
}
/**
* 设置按钮事件
*/
private void setupButtons() {
// 取消按钮
view.findViewById(R.id.btn_cancel).setOnClickListener(v -> dismiss());
// 确定按钮
view.findViewById(R.id.btn_sure).setOnClickListener(v -> {
validateAndSubmit();
});
}
/**
* 验证并提交选择
*/
private void validateAndSubmit() {
// 确保使用最新的选择器值
syncPickerValuesToCalendars();
// 验证时间范围
if ((showDate && showTime) || (showDate && !showTime)) {
if (endCalendar.getTime().before(startCalendar.getTime())) {
Toast.makeText(requireContext(), "结束时间不能早于开始时间", Toast.LENGTH_SHORT).show();
return;
}
}
// 回调监听器
if (listener != null) {
listener.onTimeRangeSelected(startCalendar.getTime(), endCalendar.getTime());
}
dismiss();
}
/**
* 同步选择器值到Calendar对象
*/
private void syncPickerValuesToCalendars() {
if (showDate) {
startCalendar.set(wheelStartDate.getYear(), wheelStartDate.getMonth(), wheelStartDate.getDay());
endCalendar.set(wheelEndDate.getYear(), wheelEndDate.getMonth(), wheelEndDate.getDay());
}
if (showTime) {
startCalendar.set(Calendar.HOUR_OF_DAY, wheelStartTime.getHour());
startCalendar.set(Calendar.MINUTE, wheelStartTime.getMinute());
if (showSeconds) {
startCalendar.set(Calendar.SECOND, wheelStartTime.getSecond());
}
endCalendar.set(Calendar.HOUR_OF_DAY, wheelEndTime.getHour());
endCalendar.set(Calendar.MINUTE, wheelEndTime.getMinute());
if (showSeconds) {
endCalendar.set(Calendar.SECOND, wheelEndTime.getSecond());
}
}
}
}

View File

@@ -1,287 +0,0 @@
package com.xscm.moduleutil.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import com.blankj.utilcode.util.ScreenUtils;
import com.xscm.moduleutil.utils.BarUtils;
/**
* com.xscm.moduleutil.widget
* qx 首页首充好礼的悬浮框
* 2025/11/4
*/
public class DropHomeView extends LinearLayout {
private int rightMargin = 0;
private float lastX, lastY;
private int screenWidth;
private int screenHeight; // 添加屏幕高度变量
public DropHomeView(Context context) {
super(context);
init();
}
public DropHomeView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DropHomeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
void init() {
// 初始化屏幕尺寸
screenWidth = ScreenUtils.getScreenWidth();
screenHeight = ScreenUtils.getScreenHeight();
post(new Runnable() {
@Override
public void run() {
//设置初始位置
int sh = ScreenUtils.getScreenHeight();
int sw = ScreenUtils.getScreenWidth();
// setBackgroundResource(R.drawable.bg_home_drop_view);
int y = (int) (0.6f * sh) - getHeight();//这是设置展示的纵坐标
// 确保Y坐标不会超出屏幕范围
y = Math.max(0, Math.min(y, sh - getHeight()));
// int x = sw - getWidth();//这是靠右边展示的
int x=20 ;//这里这只一小的数值,就是靠左展示的
setTranslationX(x);
setTranslationY(y);
}
});
updateSize();
mStatusBarHeight = BarUtils.getStatusBarHeight();
}
/**
* 更新屏幕尺寸信息
*/
protected void updateSize() {
ViewGroup viewGroup = (ViewGroup) getParent();
if (viewGroup != null) {
mScreenWidth = viewGroup.getWidth();
mScreenHeight = viewGroup.getHeight();
} else {
// 如果父视图为空,使用屏幕的实际宽度和高度
mScreenWidth = getResources().getDisplayMetrics().widthPixels;
mScreenHeight = getResources().getDisplayMetrics().heightPixels;
}
}
boolean starDrap = false;
float X1;
float X2;
float Y1;
float Y2;
// 记录视图初始位置
private float originalX;
private float originalY;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (starDrap) return true;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
X1 = event.getRawX();
Y1 = event.getRawY();
// 记录视图当前位置
originalX = getTranslationX();
originalY = getTranslationY();
break;
case MotionEvent.ACTION_MOVE:
X2 = event.getRawX();
Y2 = event.getRawY();
Action(X1, X2, Y1, Y2);
break;
}
return starDrap;
}
String TAG = "DropHourlView";
public boolean Action(float X1, float X2, float Y1, float Y2) {
float ComparedX = X2 - X1;//第二次的X坐标的位置减去第一次X坐标的位置代表X坐标上的变化情况
float ComparedY = Y2 - Y1;//同理
//当X坐标的变化量的绝对值大于Y坐标的变化量的绝对值以X坐标的变化情况作为判断依据
//上下左右的判断,都在一条直线上,但手指的操作不可能划直线,所有选择变化量大的方向上的量
//作为判断依据
if (Math.abs(ComparedX) > 30 || Math.abs(ComparedY) > 30) {
Log.i(TAG, "Action: 拖动");
starDrap = true;
// setBackgroundResource(R.drawable.bg_home_drop_view);
return true;
} else {
starDrap = false;
return false;
}
}
private float mOriginalRawX;
private float mOriginalRawY;
private float mOriginalX;
private float mOriginalY;
protected int mScreenWidth;
private int mScreenHeight;
private int mStatusBarHeight;
private void updateViewPosition(MotionEvent event) {
// 计算新的Y位置
float desY = mOriginalY + event.getRawY() - mOriginalRawY;
// 限制Y位置不超出屏幕边界
if (desY < mStatusBarHeight) {
desY = mStatusBarHeight;
}
if (desY > mScreenHeight - getHeight()) {
desY = mScreenHeight - getHeight();
}
// 计算新的X位置
float desX = mOriginalX + event.getRawX() - mOriginalRawX;
// 限制X位置不超出屏幕边界
if (desX < 0) {
desX = 0;
}
if (desX > mScreenWidth - getWidth()) {
desX = mScreenWidth - getWidth();
}
// 设置视图的新位置
setX(desX);
setY(desY);
}
private void changeOriginalTouchParams(MotionEvent event) {
mOriginalX = getX();//getX()相对于控件X坐标的距离
mOriginalY = getY();
mOriginalRawX = event.getRawX();//getRawX()指控件在屏幕上的X坐标
mOriginalRawY = event.getRawY();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
changeOriginalTouchParams(event);
updateSize(); // 添加这行确保尺寸是最新的
// ... 其他现有代码 ...
break;
case MotionEvent.ACTION_MOVE:
updateViewPosition(event); // 使用更新后的带边界检查的方法
// setBackgroundResource(R.drawable.bg_home_drop_view);
// 使用屏幕绝对坐标计算新位置
// float newX = originalX + (event.getRawX() - X1);
// float newY = originalY + (event.getRawY() - Y1);
//
// // 限制X和Y坐标在屏幕范围内
// newX = Math.max(0, Math.min(newX, screenWidth - getWidth()));
// newY = Math.max(0, Math.min(newY, screenHeight - getHeight()));
//
// setTranslationX(newX);
// setTranslationY(newY);
// X2 = event.getRawX();
break;
case MotionEvent.ACTION_UP:
starDrap = false;
int sw = ScreenUtils.getScreenWidth();
Log.i(TAG, "onTouchEvent: " + sw + "," + X2);
boolean isR = getTranslationX() + getWidth() / 2 >= sw / 2;//贴边方向
// 获取当前Y坐标
float currentY = getTranslationY();
// 创建X轴和Y轴的动画
ObjectAnimator animX = ObjectAnimator.ofFloat(this, "translationX", isR ? sw - getWidth() : 0f).setDuration(200);
// Y轴保持当前位置但确保在屏幕范围内
currentY = Math.max(0, Math.min(currentY, screenHeight - getHeight()));
ObjectAnimator animY = ObjectAnimator.ofFloat(this, "translationY", currentY).setDuration(200);
animX.start();
animY.start();
break;
}
return true;
}
public void doRevealAnimation(View mPuppet, boolean flag) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int[] vLocation = new int[2];
getLocationInWindow(vLocation);
int centerX = vLocation[0] + getMeasuredWidth() / 2;
int centerY = vLocation[1] + getMeasuredHeight() / 2;
int height = ScreenUtils.getScreenHeight();
int width = ScreenUtils.getScreenWidth();
int maxRradius = (int) Math.hypot(height, width);
Log.e("hei", maxRradius + "");
if (flag) {
mPuppet.setVisibility(VISIBLE);
Animator animator = ViewAnimationUtils.createCircularReveal(mPuppet, centerX, centerY, maxRradius, 0);
animator.setDuration(600);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mPuppet.setVisibility(View.GONE);
}
});
animator.start();
flag = false;
} else {
Animator animator = ViewAnimationUtils.createCircularReveal(mPuppet, centerX, centerY, 0, maxRradius);
animator.setDuration(1000);
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mPuppet.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
flag = true;
}
}
}
}

View File

@@ -1,52 +0,0 @@
package com.xscm.moduleutil.widget;
import android.content.Context;
import android.graphics.LinearGradient;
import android.graphics.Shader;
import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatTextView;
/**
* com.xscm.moduleutil.widget
* qx 爵位中的文字渐变颜色
* 2025/11/8
*/
public class GradientTextView extends AppCompatTextView {
public GradientTextView(Context context) {
super(context);
}
public GradientTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public GradientTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
createGradient();
}
}
private void createGradient() {
// 获取TextView的宽度
int width = getWidth();
if (width > 0) {
// 创建从左到右的线性渐变 #A292FF -> #FFFFFF -> #A292FF
LinearGradient gradient = new LinearGradient(
0, 0, width, 0,
new int[]{0xFFA292FF, 0xFFFFFFFF, 0xFFA292FF},
new float[]{0f, 0.5f, 1f},
Shader.TileMode.CLAMP
);
getPaint().setShader(gradient);
invalidate();
}
}
}

View File

@@ -1,42 +0,0 @@
package com.xscm.moduleutil.widget;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class GrayscaleTransformation extends BitmapTransformation {
private Float saturation;
public GrayscaleTransformation(float saturation) {
super();
this.saturation = saturation;
}
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
Bitmap result = pool.get(toTransform.getWidth(), toTransform.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(saturation); // 设置为灰度
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
canvas.drawBitmap(toTransform, 0, 0, paint);
return result;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update("grayscale transformation".getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -1,199 +0,0 @@
package com.xscm.moduleutil.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.TextView;
import com.xscm.moduleutil.R;
/**
* 小熊猫展示的金额,是在炼仙传说中使用的
*/
public class ShapeRedTextView extends TextView {
private final int SHAPE_RECTANGEL = 0;
private final int SHAPE_OVAL = 1;
private int shape;
private int solidNormalColor;
private int solidPressedColor;
private float cornersRadius;
private float cornersTopLeft;
private float cornersTopRight;
private float cornersBottomLeft;
private float cornersBottomRight;
// 渐变颜色属性
private int gradientNormalStartColor;
private int gradientNormalCenterColor;
private int gradientNormalEndColor;
private int gradientPressedStartColor;
private int gradientPressedCenterColor;
private int gradientPressedEndColor;
private int gradientOrientation;
private float strokeWidth;
private int strokeColor;
private int defaultColor = Color.parseColor("#00000000");
private GradientDrawable.Orientation[] orientations;
public ShapeRedTextView(Context context) {
this(context, null);
}
public ShapeRedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ShapeTextView);
shape = array.getInteger(R.styleable.ShapeTextView_shape, SHAPE_RECTANGEL);
solidNormalColor = array.getColor(R.styleable.ShapeTextView_solidNormal, defaultColor);
solidPressedColor = array.getColor(R.styleable.ShapeTextView_solidPressed, defaultColor);
cornersRadius = array.getDimension(R.styleable.ShapeTextView_cornersRadius, 0);
cornersTopLeft = array.getDimension(R.styleable.ShapeTextView_cornerTopLeft, 0);
cornersTopRight = array.getDimension(R.styleable.ShapeTextView_cornerTopRight, 0);
cornersBottomLeft = array.getDimension(R.styleable.ShapeTextView_cornerBottomLeft, 0);
cornersBottomRight = array.getDimension(R.styleable.ShapeTextView_cornerBottomRight, 0);
strokeWidth = array.getDimension(R.styleable.ShapeTextView_strokeWidth, 0);
strokeColor = array.getColor(R.styleable.ShapeTextView_strokeColor, defaultColor);
gradientNormalStartColor = array.getColor(R.styleable.ShapeTextView_gradientNormalStartColor, defaultColor);
gradientNormalCenterColor = array.getColor(R.styleable.ShapeTextView_gradientNormalCenterColor, defaultColor);
gradientNormalEndColor = array.getColor(R.styleable.ShapeTextView_gradientNormalEndColor, defaultColor);
gradientPressedStartColor = array.getColor(R.styleable.ShapeTextView_gradientPressedStartColor, defaultColor);
gradientPressedCenterColor = array.getColor(R.styleable.ShapeTextView_gradientPressedCenterColor, defaultColor);
gradientPressedEndColor = array.getColor(R.styleable.ShapeTextView_gradientPressedEndColor, defaultColor);
TypedArray orientationArray = context.obtainStyledAttributes(attrs, R.styleable.ShapeTextView);
gradientOrientation = orientationArray.getInteger(R.styleable.ShapeTextView_gradientOrientation, 6);
array.recycle();
orientations = new GradientDrawable.Orientation[]{
GradientDrawable.Orientation.TOP_BOTTOM,
GradientDrawable.Orientation.TR_BL,
GradientDrawable.Orientation.RIGHT_LEFT,
GradientDrawable.Orientation.BR_TL,
GradientDrawable.Orientation.BOTTOM_TOP,
GradientDrawable.Orientation.BL_TR,
GradientDrawable.Orientation.LEFT_RIGHT,
GradientDrawable.Orientation.TL_BR
};
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setShape();
}
private void setShape() {
setGravity(Gravity.CENTER);
setClickable(true);
// normal state
GradientDrawable drawableNormal = new GradientDrawable();
// 设置Shape
drawableNormal.setShape(shape);
// 设置圆角半径
drawableNormal.setCornerRadius(cornersRadius);
// 圆角半径(每个圆角半径的值)
if (cornersRadius == 0) {
drawableNormal.setCornerRadii(new float[]{
cornersTopLeft, cornersTopLeft,
cornersTopRight, cornersTopRight,
cornersBottomRight, cornersBottomRight,
cornersBottomLeft, cornersBottomLeft});
}
//描边的宽度和颜色
drawableNormal.setStroke((int) strokeWidth, strokeColor);
//设置填充色
if (solidNormalColor != defaultColor) {
drawableNormal.setColor(solidNormalColor);
} else {
// 设置渐变色
int[] gradientColors;
if (gradientNormalStartColor != defaultColor && gradientNormalEndColor != defaultColor) {
gradientColors = new int[]{gradientNormalStartColor, gradientNormalEndColor};
if (gradientNormalCenterColor != defaultColor) {
gradientColors = new int[]{gradientNormalStartColor, gradientNormalCenterColor, gradientNormalEndColor};
}
drawableNormal.setColors(gradientColors);
drawableNormal.setOrientation(orientations[gradientOrientation]);
} else {
drawableNormal.setColor(solidNormalColor);
}
}
// pressed state
GradientDrawable drawablePressed = new GradientDrawable();
drawablePressed.setShape(shape);
drawablePressed.setCornerRadius(cornersRadius);
if (cornersRadius == 0) {
drawablePressed.setCornerRadii(new float[]{
cornersTopLeft, cornersTopLeft,
cornersTopRight, cornersTopRight,
cornersBottomRight, cornersBottomRight,
cornersBottomLeft, cornersBottomLeft});
}
drawablePressed.setStroke((int) strokeWidth, strokeColor);
drawablePressed.setColor(solidPressedColor);
// 设置背景选择器
StateListDrawable stateListDrawable = new StateListDrawable();
if (solidPressedColor != defaultColor) {
stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePressed);
}
int[] gradientPressdColors;
if (gradientPressedStartColor != defaultColor && gradientPressedEndColor != defaultColor) {
gradientPressdColors = new int[]{gradientPressedStartColor, gradientPressedEndColor};
if (gradientPressedCenterColor != defaultColor) {
gradientPressdColors = new int[]{gradientPressedStartColor, gradientPressedCenterColor, gradientPressedEndColor};
}
drawablePressed.setColors(gradientPressdColors);
drawablePressed.setOrientation(orientations[gradientOrientation]);
stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePressed);
}
if (isEnabled()) {
setAlpha(1.0f);
} else {
setAlpha(0.7f);
}
stateListDrawable.addState(new int[]{}, drawableNormal);
setBackground(stateListDrawable);
}
}

View File

@@ -1,135 +0,0 @@
package com.xscm.moduleutil.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Matrix
import android.graphics.Shader
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import com.xscm.moduleutil.R
/**
*
* 流光字体
* @author TXZ
* @version 1.0
* created by 2024/5/23 9:32
*/
class ShineTextView : AppCompatTextView {
//是否开启流光,默认开启
var isShine = true
//默认是流光效果 0 流光 1 注入效果
var shineType = 0
//流光效果下字体流动次数
var shineCount: Int = Int.MAX_VALUE
//注入效果 开始,中间,结束
var startColor: Int = Color.BLACK
var shineColor: Int = Color.BLACK
var endColor: Int = Color.BLACK
//一次动效时长
var shineDuration: Int = 1000
var _count: Int = 0 //自行运行动画次数
private var mLinearGradient: LinearGradient? = null
private var mGradientMatrix: Matrix = Matrix()
private var mViewWidth = 0
private var mTranslate = 0
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
) {
obtainAttributes(attrs!!)
}
private fun obtainAttributes(attrs: AttributeSet) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ShineTextView)
typedArray.apply {
isShine = typedArray.getBoolean(R.styleable.ShineTextView_isShine, isShine)
shineType = typedArray.getInt(R.styleable.ShineTextView_shineType, shineType)
startColor = typedArray.getColor(R.styleable.ShineTextView_startColor, startColor)
shineColor = typedArray.getColor(R.styleable.ShineTextView_midColor, shineColor)
endColor = typedArray.getColor(R.styleable.ShineTextView_endColor, endColor)
shineCount = typedArray.getInt(R.styleable.ShineTextView_shineCount, shineCount)
shineDuration =
typedArray.getInt(R.styleable.ShineTextView_shineDuration, shineDuration)
}
typedArray.recycle()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (isShine) {
mViewWidth = measuredWidth
when (shineType) {
0 -> {
//流光效果
mLinearGradient = LinearGradient(
0f,
0f,
(mViewWidth / 1).toFloat(),
0f,
intArrayOf(currentTextColor, shineColor, currentTextColor),
null,
Shader.TileMode.CLAMP
)
}
1 -> {
mLinearGradient = LinearGradient(
0f,
0f,
(mViewWidth / 6).toFloat(),
0f,
intArrayOf(endColor, shineColor, startColor),
null,
Shader.TileMode.CLAMP
)
}
}
paint.shader = mLinearGradient
}else {
mLinearGradient = LinearGradient(
0f,
0f,
0f,
0f,
intArrayOf(Color.BLACK,Color.BLACK),
null,
Shader.TileMode.CLAMP
)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (!isShine) return
mTranslate += mViewWidth / (shineDuration / 50)
if (mTranslate > 1.2 * mViewWidth) {
mTranslate = -mViewWidth / 5
_count++
}
mGradientMatrix.setTranslate(mTranslate.toFloat(), 0f)
mLinearGradient?.setLocalMatrix(mGradientMatrix)
when {
shineType == 0 && _count < shineCount -> postInvalidateDelayed(50)
shineType == 1 && _count < 1 -> postInvalidateDelayed(50)
else -> {
mGradientMatrix.setTranslate((1.2 * mViewWidth).toFloat(), 0f)
mLinearGradient?.setLocalMatrix(mGradientMatrix)
}
}
}
}

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="2000"
android:fromYDelta="0"
android:toYDelta="-20"
android:repeatCount="infinite"
android:repeatMode="reverse"
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
</set>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="@color/divider" />
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
</shape>

Some files were not shown because too many files have changed in this diff Show More