From a6b56bceb182641cc9e85fce64f1da2565030372 Mon Sep 17 00:00:00 2001 From: ShiQi <3572915148@qq.com> Date: Mon, 22 Dec 2025 16:31:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=B8=AD=E5=90=84=E4=B8=AA=E7=BB=86=E8=8A=82=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android-app/app/src/main/AndroidManifest.xml | 33 + .../app/src/main/assets/location_data.json | 657 ++++++++++++++++++ .../livestreaming/AvatarViewerDialog.java | 162 +++++ .../livestreaming/ConversationActivity.java | 18 + .../ConversationMessagesAdapter.java | 118 ++++ .../livestreaming/ConversationsAdapter.java | 50 ++ .../livestreaming/DrawGuessActivity.java | 61 ++ .../livestreaming/EditProfileActivity.java | 308 ++++++++ .../livestreaming/FindGameActivity.java | 61 ++ .../livestreaming/FishPondActivity.java | 100 ++- .../HeartbeatSignalActivity.java | 62 ++ .../livestreaming/KTVTogetherActivity.java | 61 ++ .../livestreaming/LocationDataManager.java | 142 ++++ .../example/livestreaming/MainActivity.java | 316 ++++++++- .../livestreaming/MessagesActivity.java | 182 ++++- .../livestreaming/NearbyUsersAdapter.java | 45 +- .../livestreaming/OnlineDatingActivity.java | 61 ++ .../livestreaming/PeaceEliteActivity.java | 61 ++ .../livestreaming/ProfileActivity.java | 223 +++++- .../example/livestreaming/SearchActivity.java | 16 + .../livestreaming/TabPlaceholderActivity.java | 17 +- .../livestreaming/TableGamesActivity.java | 61 ++ .../livestreaming/UnreadMessageManager.java | 74 ++ .../UserProfileReadOnlyActivity.java | 16 +- .../livestreaming/VoiceMatchActivity.java | 62 ++ .../app/src/main/res/drawable/ic_globe_24.xml | 2 +- .../main/res/layout/activity_draw_guess.xml | 56 ++ .../main/res/layout/activity_edit_profile.xml | 24 +- .../main/res/layout/activity_find_game.xml | 56 ++ .../main/res/layout/activity_fish_pond.xml | 10 +- .../res/layout/activity_heartbeat_signal.xml | 56 ++ .../main/res/layout/activity_ktv_together.xml | 56 ++ .../app/src/main/res/layout/activity_main.xml | 20 +- .../res/layout/activity_online_dating.xml | 56 ++ .../main/res/layout/activity_peace_elite.xml | 56 ++ .../src/main/res/layout/activity_profile.xml | 33 +- .../main/res/layout/activity_table_games.xml | 56 ++ .../activity_user_profile_read_only.xml | 274 ++++++-- .../main/res/layout/activity_voice_match.xml | 56 ++ .../res/layout/bottom_sheet_date_picker.xml | 107 +++ .../layout/bottom_sheet_location_picker.xml | 80 +++ .../bottom_sheet_location_picker_spinner.xml | 100 +++ .../main/res/layout/dialog_avatar_viewer.xml | 20 + .../main/res/layout/include_bottom_nav.xml | 38 +- .../src/main/res/layout/item_conversation.xml | 5 +- .../item_conversation_message_incoming.xml | 5 +- .../item_conversation_message_outgoing.xml | 5 +- .../src/main/res/layout/item_nearby_user.xml | 138 ++-- .../app/src/main/res/values/themes.xml | 5 + 49 files changed, 4017 insertions(+), 264 deletions(-) create mode 100644 android-app/app/src/main/assets/location_data.json create mode 100644 android-app/app/src/main/java/com/example/livestreaming/AvatarViewerDialog.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/DrawGuessActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/LocationDataManager.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java create mode 100644 android-app/app/src/main/res/layout/activity_draw_guess.xml create mode 100644 android-app/app/src/main/res/layout/activity_find_game.xml create mode 100644 android-app/app/src/main/res/layout/activity_heartbeat_signal.xml create mode 100644 android-app/app/src/main/res/layout/activity_ktv_together.xml create mode 100644 android-app/app/src/main/res/layout/activity_online_dating.xml create mode 100644 android-app/app/src/main/res/layout/activity_peace_elite.xml create mode 100644 android-app/app/src/main/res/layout/activity_table_games.xml create mode 100644 android-app/app/src/main/res/layout/activity_voice_match.xml create mode 100644 android-app/app/src/main/res/layout/bottom_sheet_date_picker.xml create mode 100644 android-app/app/src/main/res/layout/bottom_sheet_location_picker.xml create mode 100644 android-app/app/src/main/res/layout/bottom_sheet_location_picker_spinner.xml create mode 100644 android-app/app/src/main/res/layout/dialog_avatar_viewer.xml diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index d77b2ef8..01deb8dd 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/assets/location_data.json b/android-app/app/src/main/assets/location_data.json new file mode 100644 index 00000000..01b2a7ff --- /dev/null +++ b/android-app/app/src/main/assets/location_data.json @@ -0,0 +1,657 @@ +{ + "provinces": [ + { + "name": "北京", + "cities": [ + {"name": "东城区"}, + {"name": "西城区"}, + {"name": "朝阳区"}, + {"name": "丰台区"}, + {"name": "石景山区"}, + {"name": "海淀区"}, + {"name": "门头沟区"}, + {"name": "房山区"}, + {"name": "通州区"}, + {"name": "顺义区"}, + {"name": "昌平区"}, + {"name": "大兴区"}, + {"name": "怀柔区"}, + {"name": "平谷区"}, + {"name": "密云区"}, + {"name": "延庆区"} + ] + }, + { + "name": "上海", + "cities": [ + {"name": "黄浦区"}, + {"name": "徐汇区"}, + {"name": "长宁区"}, + {"name": "静安区"}, + {"name": "普陀区"}, + {"name": "虹口区"}, + {"name": "杨浦区"}, + {"name": "闵行区"}, + {"name": "宝山区"}, + {"name": "嘉定区"}, + {"name": "浦东新区"}, + {"name": "金山区"}, + {"name": "松江区"}, + {"name": "青浦区"}, + {"name": "奉贤区"}, + {"name": "崇明区"} + ] + }, + { + "name": "天津", + "cities": [ + {"name": "和平区"}, + {"name": "河东区"}, + {"name": "河西区"}, + {"name": "南开区"}, + {"name": "河北区"}, + {"name": "红桥区"}, + {"name": "东丽区"}, + {"name": "西青区"}, + {"name": "津南区"}, + {"name": "北辰区"}, + {"name": "武清区"}, + {"name": "宝坻区"}, + {"name": "滨海新区"}, + {"name": "宁河区"}, + {"name": "静海区"}, + {"name": "蓟州区"} + ] + }, + { + "name": "重庆", + "cities": [ + {"name": "万州区"}, + {"name": "涪陵区"}, + {"name": "渝中区"}, + {"name": "大渡口区"}, + {"name": "江北区"}, + {"name": "沙坪坝区"}, + {"name": "九龙坡区"}, + {"name": "南岸区"}, + {"name": "北碚区"}, + {"name": "綦江区"}, + {"name": "大足区"}, + {"name": "渝北区"}, + {"name": "巴南区"}, + {"name": "黔江区"}, + {"name": "长寿区"}, + {"name": "江津区"}, + {"name": "合川区"}, + {"name": "永川区"}, + {"name": "南川区"}, + {"name": "璧山区"}, + {"name": "铜梁区"}, + {"name": "潼南区"}, + {"name": "荣昌区"}, + {"name": "开州区"}, + {"name": "梁平区"}, + {"name": "武隆区"} + ] + }, + { + "name": "河北", + "cities": [ + {"name": "石家庄"}, + {"name": "唐山"}, + {"name": "秦皇岛"}, + {"name": "邯郸"}, + {"name": "邢台"}, + {"name": "保定"}, + {"name": "张家口"}, + {"name": "承德"}, + {"name": "沧州"}, + {"name": "廊坊"}, + {"name": "衡水"} + ] + }, + { + "name": "山西", + "cities": [ + {"name": "太原"}, + {"name": "大同"}, + {"name": "阳泉"}, + {"name": "长治"}, + {"name": "晋城"}, + {"name": "朔州"}, + {"name": "晋中"}, + {"name": "运城"}, + {"name": "忻州"}, + {"name": "临汾"}, + {"name": "吕梁"} + ] + }, + { + "name": "内蒙古", + "cities": [ + {"name": "呼和浩特"}, + {"name": "包头"}, + {"name": "乌海"}, + {"name": "赤峰"}, + {"name": "通辽"}, + {"name": "鄂尔多斯"}, + {"name": "呼伦贝尔"}, + {"name": "巴彦淖尔"}, + {"name": "乌兰察布"}, + {"name": "兴安盟"}, + {"name": "锡林郭勒盟"}, + {"name": "阿拉善盟"} + ] + }, + { + "name": "辽宁", + "cities": [ + {"name": "沈阳"}, + {"name": "大连"}, + {"name": "鞍山"}, + {"name": "抚顺"}, + {"name": "本溪"}, + {"name": "丹东"}, + {"name": "锦州"}, + {"name": "营口"}, + {"name": "阜新"}, + {"name": "辽阳"}, + {"name": "盘锦"}, + {"name": "铁岭"}, + {"name": "朝阳"}, + {"name": "葫芦岛"} + ] + }, + { + "name": "吉林", + "cities": [ + {"name": "长春"}, + {"name": "吉林"}, + {"name": "四平"}, + {"name": "辽源"}, + {"name": "通化"}, + {"name": "白山"}, + {"name": "松原"}, + {"name": "白城"}, + {"name": "延边朝鲜族自治州"} + ] + }, + { + "name": "黑龙江", + "cities": [ + {"name": "哈尔滨"}, + {"name": "齐齐哈尔"}, + {"name": "鸡西"}, + {"name": "鹤岗"}, + {"name": "双鸭山"}, + {"name": "大庆"}, + {"name": "伊春"}, + {"name": "佳木斯"}, + {"name": "七台河"}, + {"name": "牡丹江"}, + {"name": "黑河"}, + {"name": "绥化"}, + {"name": "大兴安岭"} + ] + }, + { + "name": "江苏", + "cities": [ + {"name": "南京"}, + {"name": "无锡"}, + {"name": "徐州"}, + {"name": "常州"}, + {"name": "苏州"}, + {"name": "南通"}, + {"name": "连云港"}, + {"name": "淮安"}, + {"name": "盐城"}, + {"name": "扬州"}, + {"name": "镇江"}, + {"name": "泰州"}, + {"name": "宿迁"} + ] + }, + { + "name": "浙江", + "cities": [ + {"name": "杭州"}, + {"name": "宁波"}, + {"name": "温州"}, + {"name": "嘉兴"}, + {"name": "湖州"}, + {"name": "绍兴"}, + {"name": "金华"}, + {"name": "衢州"}, + {"name": "舟山"}, + {"name": "台州"}, + {"name": "丽水"} + ] + }, + { + "name": "安徽", + "cities": [ + {"name": "合肥"}, + {"name": "芜湖"}, + {"name": "蚌埠"}, + {"name": "淮南"}, + {"name": "马鞍山"}, + {"name": "淮北"}, + {"name": "铜陵"}, + {"name": "安庆"}, + {"name": "黄山"}, + {"name": "滁州"}, + {"name": "阜阳"}, + {"name": "宿州"}, + {"name": "六安"}, + {"name": "亳州"}, + {"name": "池州"}, + {"name": "宣城"} + ] + }, + { + "name": "福建", + "cities": [ + {"name": "福州"}, + {"name": "厦门"}, + {"name": "莆田"}, + {"name": "三明"}, + {"name": "泉州"}, + {"name": "漳州"}, + {"name": "南平"}, + {"name": "龙岩"}, + {"name": "宁德"} + ] + }, + { + "name": "江西", + "cities": [ + {"name": "南昌"}, + {"name": "景德镇"}, + {"name": "萍乡"}, + {"name": "九江"}, + {"name": "新余"}, + {"name": "鹰潭"}, + {"name": "赣州"}, + {"name": "吉安"}, + {"name": "宜春"}, + {"name": "抚州"}, + {"name": "上饶"} + ] + }, + { + "name": "山东", + "cities": [ + {"name": "济南"}, + {"name": "青岛"}, + {"name": "淄博"}, + {"name": "枣庄"}, + {"name": "东营"}, + {"name": "烟台"}, + {"name": "潍坊"}, + {"name": "济宁"}, + {"name": "泰安"}, + {"name": "威海"}, + {"name": "日照"}, + {"name": "临沂"}, + {"name": "德州"}, + {"name": "聊城"}, + {"name": "滨州"}, + {"name": "菏泽"} + ] + }, + { + "name": "河南", + "cities": [ + {"name": "郑州"}, + {"name": "开封"}, + {"name": "洛阳"}, + {"name": "平顶山"}, + {"name": "安阳"}, + {"name": "鹤壁"}, + {"name": "新乡"}, + {"name": "焦作"}, + {"name": "濮阳"}, + {"name": "许昌"}, + {"name": "漯河"}, + {"name": "三门峡"}, + {"name": "南阳"}, + {"name": "商丘"}, + {"name": "信阳"}, + {"name": "周口"}, + {"name": "驻马店"}, + {"name": "济源"} + ] + }, + { + "name": "湖北", + "cities": [ + {"name": "武汉"}, + {"name": "黄石"}, + {"name": "十堰"}, + {"name": "宜昌"}, + {"name": "襄阳"}, + {"name": "鄂州"}, + {"name": "荆门"}, + {"name": "孝感"}, + {"name": "荆州"}, + {"name": "黄冈"}, + {"name": "咸宁"}, + {"name": "随州"}, + {"name": "恩施"}, + {"name": "仙桃"}, + {"name": "潜江"}, + {"name": "天门"}, + {"name": "神农架"} + ] + }, + { + "name": "湖南", + "cities": [ + {"name": "长沙"}, + {"name": "株洲"}, + {"name": "湘潭"}, + {"name": "衡阳"}, + {"name": "邵阳"}, + {"name": "岳阳"}, + {"name": "常德"}, + {"name": "张家界"}, + {"name": "益阳"}, + {"name": "郴州"}, + {"name": "永州"}, + {"name": "怀化"}, + {"name": "娄底"}, + {"name": "湘西"} + ] + }, + { + "name": "广东", + "cities": [ + {"name": "广州"}, + {"name": "深圳"}, + {"name": "珠海"}, + {"name": "汕头"}, + {"name": "佛山"}, + {"name": "韶关"}, + {"name": "湛江"}, + {"name": "肇庆"}, + {"name": "江门"}, + {"name": "茂名"}, + {"name": "惠州"}, + {"name": "梅州"}, + {"name": "汕尾"}, + {"name": "河源"}, + {"name": "阳江"}, + {"name": "清远"}, + {"name": "东莞"}, + {"name": "中山"}, + {"name": "潮州"}, + {"name": "揭阳"}, + {"name": "云浮"} + ] + }, + { + "name": "广西", + "cities": [ + {"name": "南宁"}, + {"name": "柳州"}, + {"name": "桂林"}, + {"name": "梧州"}, + {"name": "北海"}, + {"name": "防城港"}, + {"name": "钦州"}, + {"name": "贵港"}, + {"name": "玉林"}, + {"name": "百色"}, + {"name": "贺州"}, + {"name": "河池"}, + {"name": "来宾"}, + {"name": "崇左"} + ] + }, + { + "name": "海南", + "cities": [ + {"name": "海口"}, + {"name": "三亚"}, + {"name": "三沙"}, + {"name": "儋州"}, + {"name": "五指山"}, + {"name": "琼海"}, + {"name": "文昌"}, + {"name": "万宁"}, + {"name": "东方"}, + {"name": "定安"}, + {"name": "屯昌"}, + {"name": "澄迈"}, + {"name": "临高"}, + {"name": "白沙"}, + {"name": "昌江"}, + {"name": "乐东"}, + {"name": "陵水"}, + {"name": "保亭"}, + {"name": "琼中"} + ] + }, + { + "name": "四川", + "cities": [ + {"name": "成都"}, + {"name": "自贡"}, + {"name": "攀枝花"}, + {"name": "泸州"}, + {"name": "德阳"}, + {"name": "绵阳"}, + {"name": "广元"}, + {"name": "遂宁"}, + {"name": "内江"}, + {"name": "乐山"}, + {"name": "南充"}, + {"name": "眉山"}, + {"name": "宜宾"}, + {"name": "广安"}, + {"name": "达州"}, + {"name": "雅安"}, + {"name": "巴中"}, + {"name": "资阳"}, + {"name": "阿坝"}, + {"name": "甘孜"}, + {"name": "凉山"} + ] + }, + { + "name": "贵州", + "cities": [ + {"name": "贵阳"}, + {"name": "六盘水"}, + {"name": "遵义"}, + {"name": "安顺"}, + {"name": "毕节"}, + {"name": "铜仁"}, + {"name": "黔西南"}, + {"name": "黔东南"}, + {"name": "黔南"} + ] + }, + { + "name": "云南", + "cities": [ + {"name": "昆明"}, + {"name": "曲靖"}, + {"name": "玉溪"}, + {"name": "保山"}, + {"name": "昭通"}, + {"name": "丽江"}, + {"name": "普洱"}, + {"name": "临沧"}, + {"name": "楚雄"}, + {"name": "红河"}, + {"name": "文山"}, + {"name": "西双版纳"}, + {"name": "大理"}, + {"name": "德宏"}, + {"name": "怒江"}, + {"name": "迪庆"} + ] + }, + { + "name": "西藏", + "cities": [ + {"name": "拉萨"}, + {"name": "日喀则"}, + {"name": "昌都"}, + {"name": "林芝"}, + {"name": "山南"}, + {"name": "那曲"}, + {"name": "阿里"} + ] + }, + { + "name": "陕西", + "cities": [ + {"name": "西安"}, + {"name": "铜川"}, + {"name": "宝鸡"}, + {"name": "咸阳"}, + {"name": "渭南"}, + {"name": "延安"}, + {"name": "汉中"}, + {"name": "榆林"}, + {"name": "安康"}, + {"name": "商洛"} + ] + }, + { + "name": "甘肃", + "cities": [ + {"name": "兰州"}, + {"name": "嘉峪关"}, + {"name": "金昌"}, + {"name": "白银"}, + {"name": "天水"}, + {"name": "武威"}, + {"name": "张掖"}, + {"name": "平凉"}, + {"name": "酒泉"}, + {"name": "庆阳"}, + {"name": "定西"}, + {"name": "陇南"}, + {"name": "临夏"}, + {"name": "甘南"} + ] + }, + { + "name": "青海", + "cities": [ + {"name": "西宁"}, + {"name": "海东"}, + {"name": "海北"}, + {"name": "黄南"}, + {"name": "海南"}, + {"name": "果洛"}, + {"name": "玉树"}, + {"name": "海西"} + ] + }, + { + "name": "宁夏", + "cities": [ + {"name": "银川"}, + {"name": "石嘴山"}, + {"name": "吴忠"}, + {"name": "固原"}, + {"name": "中卫"} + ] + }, + { + "name": "新疆", + "cities": [ + {"name": "乌鲁木齐"}, + {"name": "克拉玛依"}, + {"name": "吐鲁番"}, + {"name": "哈密"}, + {"name": "昌吉"}, + {"name": "博尔塔拉"}, + {"name": "巴音郭楞"}, + {"name": "阿克苏"}, + {"name": "克孜勒苏"}, + {"name": "喀什"}, + {"name": "和田"}, + {"name": "伊犁"}, + {"name": "塔城"}, + {"name": "阿勒泰"}, + {"name": "石河子"}, + {"name": "阿拉尔"}, + {"name": "图木舒克"}, + {"name": "五家渠"}, + {"name": "北屯"}, + {"name": "铁门关"}, + {"name": "双河"}, + {"name": "可克达拉"}, + {"name": "昆玉"}, + {"name": "胡杨河"} + ] + }, + { + "name": "台湾", + "cities": [ + {"name": "台北"}, + {"name": "新北"}, + {"name": "桃园"}, + {"name": "台中"}, + {"name": "台南"}, + {"name": "高雄"}, + {"name": "基隆"}, + {"name": "新竹"}, + {"name": "嘉义"}, + {"name": "宜兰"}, + {"name": "苗栗"}, + {"name": "彰化"}, + {"name": "南投"}, + {"name": "云林"}, + {"name": "屏东"}, + {"name": "台东"}, + {"name": "花莲"}, + {"name": "澎湖"}, + {"name": "金门"}, + {"name": "连江"} + ] + }, + { + "name": "香港", + "cities": [ + {"name": "中西区"}, + {"name": "湾仔区"}, + {"name": "东区"}, + {"name": "南区"}, + {"name": "深水埗区"}, + {"name": "油尖旺区"}, + {"name": "九龙城区"}, + {"name": "黄大仙区"}, + {"name": "观塘区"}, + {"name": "荃湾区"}, + {"name": "屯门区"}, + {"name": "元朗区"}, + {"name": "北区"}, + {"name": "大埔区"}, + {"name": "沙田区"}, + {"name": "西贡区"}, + {"name": "葵青区"}, + {"name": "离岛区"} + ] + }, + { + "name": "澳门", + "cities": [ + {"name": "花地玛堂区"}, + {"name": "圣安多尼堂区"}, + {"name": "望德堂区"}, + {"name": "大堂区"}, + {"name": "风顺堂区"}, + {"name": "嘉模堂区"}, + {"name": "路凼填海区"}, + {"name": "圣方济各堂区"} + ] + } + ] +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/AvatarViewerDialog.java b/android-app/app/src/main/java/com/example/livestreaming/AvatarViewerDialog.java new file mode 100644 index 00000000..6a7e5355 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/AvatarViewerDialog.java @@ -0,0 +1,162 @@ +package com.example.livestreaming; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; + +public class AvatarViewerDialog extends Dialog { + + private ImageView avatarImageView; + private Uri avatarUri; + private Integer avatarResId; + private Drawable avatarDrawable; + private Context activityContext; + + public AvatarViewerDialog(@NonNull Context context) { + super(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen); + this.activityContext = context; + } + + public static AvatarViewerDialog create(Context context) { + return new AvatarViewerDialog(context); + } + + public AvatarViewerDialog setAvatarUri(Uri uri) { + this.avatarUri = uri; + this.avatarResId = null; + this.avatarDrawable = null; + return this; + } + + public AvatarViewerDialog setAvatarResId(int resId) { + this.avatarResId = resId; + this.avatarUri = null; + this.avatarDrawable = null; + return this; + } + + public AvatarViewerDialog setAvatarDrawable(Drawable drawable) { + this.avatarDrawable = drawable; + this.avatarUri = null; + this.avatarResId = null; + return this; + } + + public AvatarViewerDialog setAvatarFromImageView(ImageView imageView) { + if (imageView == null) return this; + + // 优先从ImageView的Drawable获取 + Drawable drawable = imageView.getDrawable(); + if (drawable != null) { + return setAvatarDrawable(drawable); + } + + // 如果Drawable为空,尝试从Tag获取URI或资源ID + Object tag = imageView.getTag(); + if (tag instanceof Uri) { + return setAvatarUri((Uri) tag); + } else if (tag instanceof Integer) { + return setAvatarResId((Integer) tag); + } + + return this; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.dialog_avatar_viewer); + + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.width = WindowManager.LayoutParams.MATCH_PARENT; + params.height = WindowManager.LayoutParams.MATCH_PARENT; + window.setAttributes(params); + window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + window.setBackgroundDrawableResource(android.R.color.black); + } + + avatarImageView = findViewById(R.id.avatarImageView); + + if (avatarImageView == null) { + dismiss(); + return; + } + + // 点击任意位置关闭 + View rootLayout = findViewById(R.id.rootLayout); + if (rootLayout != null) { + rootLayout.setOnClickListener(v -> dismiss()); + } + + // 也允许点击ImageView关闭 + avatarImageView.setOnClickListener(v -> dismiss()); + + loadAvatar(); + } + + private void loadAvatar() { + if (avatarImageView == null) return; + + // 使用Activity的Context而不是Dialog的Context,确保Glide能正常工作 + Context context = activityContext; + if (context == null) { + context = getContext(); + } + if (context == null) { + avatarImageView.setImageResource(R.drawable.ic_account_circle_24); + return; + } + + try { + if (avatarUri != null) { + Glide.with(context) + .load(avatarUri) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .placeholder(R.drawable.ic_account_circle_24) + .error(R.drawable.ic_account_circle_24) + .into(avatarImageView); + } else if (avatarResId != null && avatarResId != 0) { + Glide.with(context) + .load(avatarResId) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .placeholder(R.drawable.ic_account_circle_24) + .error(R.drawable.ic_account_circle_24) + .into(avatarImageView); + } else if (avatarDrawable != null) { + // 直接设置Drawable,但如果是Glide的Drawable,可能需要特殊处理 + try { + avatarImageView.setImageDrawable(avatarDrawable); + } catch (Exception e) { + // 如果设置Drawable失败,尝试使用Glide加载 + // 对于Glide加载的Drawable,我们需要重新加载原始资源 + avatarImageView.setImageResource(R.drawable.ic_account_circle_24); + } + } else { + avatarImageView.setImageResource(R.drawable.ic_account_circle_24); + } + } catch (Exception e) { + // 如果Glide加载失败,直接使用setImageResource + try { + avatarImageView.setImageResource(R.drawable.ic_account_circle_24); + } catch (Exception ex) { + // 最后的fallback + } + } + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java index 09d5a707..e7676a30 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java @@ -33,6 +33,10 @@ public class ConversationActivity extends AppCompatActivity { context.startActivity(intent); } + private static final String EXTRA_UNREAD_COUNT = "extra_unread_count"; + + private int initialUnreadCount = 0; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -41,11 +45,25 @@ public class ConversationActivity extends AppCompatActivity { String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null; binding.titleText.setText(title != null ? title : "会话"); + + // 获取该会话的初始未读数量 + initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0; binding.backButton.setOnClickListener(v -> finish()); setupMessages(); setupInput(); + + // 用户进入会话时,标记该会话的消息为已读,减少未读数量 + if (initialUnreadCount > 0) { + UnreadMessageManager.decrementUnreadCount(this, initialUnreadCount); + } + } + + @Override + protected void onPause() { + super.onPause(); + // 当用户离开会话时,更新总未读数量(如果会话未读数量已减少) } private void setupMessages() { diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java index 6593f8d4..b611abea 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java @@ -1,8 +1,13 @@ package com.example.livestreaming; +import android.graphics.Outline; +import android.net.Uri; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; @@ -10,6 +15,8 @@ import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -56,15 +63,35 @@ public class ConversationMessagesAdapter extends ListAdapter { + if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) { + avatarView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()); + } + }); + avatarView.setClipToOutline(true); + } + }); } void bind(ChatMessage message) { @@ -72,23 +99,114 @@ public class ConversationMessagesAdapter extends ListAdapter { + if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) { + avatarView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()); + } + }); + avatarView.setClipToOutline(true); + } + }); } void bind(ChatMessage message) { if (message == null) return; msgText.setText(message.getMessage() != null ? message.getMessage() : ""); timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp()))); + + // 确保头像圆形裁剪设置正确 + setupAvatarOutline(); + // 加载用户头像 + loadAvatar(); + } + + private void loadAvatar() { + if (avatarView == null) return; + + try { + String avatarUri = avatarView.getContext() + .getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE) + .getString("profile_avatar_uri", null); + + if (!TextUtils.isEmpty(avatarUri)) { + Glide.with(avatarView) + .load(Uri.parse(avatarUri)) + .circleCrop() + .error(R.drawable.ic_account_circle_24) + .into(avatarView); + return; + } + + int avatarRes = avatarView.getContext() + .getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE) + .getInt("profile_avatar_res", 0); + + if (avatarRes != 0) { + Glide.with(avatarView) + .load(avatarRes) + .circleCrop() + .error(R.drawable.ic_account_circle_24) + .into(avatarView); + } else { + Glide.with(avatarView) + .load(R.drawable.ic_account_circle_24) + .circleCrop() + .into(avatarView); + } + } catch (Exception e) { + Glide.with(avatarView) + .load(R.drawable.ic_account_circle_24) + .circleCrop() + .into(avatarView); + } } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java index 61633e78..c9f18959 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java @@ -1,14 +1,19 @@ package com.example.livestreaming; +import android.graphics.Outline; +import android.net.Uri; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import androidx.annotation.NonNull; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; import com.example.livestreaming.databinding.ItemConversationBinding; public class ConversationsAdapter extends ListAdapter { @@ -66,11 +71,56 @@ public class ConversationsAdapter extends ListAdapter { if (item == null) return; if (onConversationClickListener != null) onConversationClickListener.onConversationClick(item); }); } + + private void loadAvatar(ConversationItem item) { + if (binding.avatar == null) return; + + // 设置圆形裁剪 + setupAvatarOutline(); + + // 根据会话ID或标题加载对应的头像 + // 这里可以根据实际需求从服务器或本地加载 + // 暂时使用默认头像,或者根据会话类型加载不同头像 + try { + String conversationId = item != null ? item.getId() : null; + + // 可以根据conversationId从SharedPreferences或其他地方加载头像 + // 这里先使用默认头像,使用Glide确保圆形裁剪 + Glide.with(binding.avatar) + .load(R.drawable.ic_account_circle_24) + .circleCrop() + .into(binding.avatar); + } catch (Exception e) { + Glide.with(binding.avatar) + .load(R.drawable.ic_account_circle_24) + .circleCrop() + .into(binding.avatar); + } + } + + private void setupAvatarOutline() { + if (binding.avatar == null) return; + // 设置圆形裁剪,确保在模拟器上也能正常工作 + binding.avatar.post(() -> { + if (binding.avatar.getWidth() > 0 && binding.avatar.getHeight() > 0) { + binding.avatar.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()); + } + }); + binding.avatar.setClipToOutline(true); + } + }); + } } private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { diff --git a/android-app/app/src/main/java/com/example/livestreaming/DrawGuessActivity.java b/android-app/app/src/main/java/com/example/livestreaming/DrawGuessActivity.java new file mode 100644 index 00000000..b1248e03 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/DrawGuessActivity.java @@ -0,0 +1,61 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityDrawGuessBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class DrawGuessActivity extends AppCompatActivity { + + private ActivityDrawGuessBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, DrawGuessActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityDrawGuessBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java index 346b4248..f227a942 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java @@ -4,25 +4,41 @@ import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Spinner; +import android.widget.TextView; import android.widget.Toast; +import android.widget.AdapterView; import androidx.core.content.ContextCompat; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.example.livestreaming.databinding.ActivityEditProfileBinding; import com.google.android.material.bottomsheet.BottomSheetDialog; +import android.widget.NumberPicker; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; +import java.util.Locale; public class EditProfileActivity extends AppCompatActivity { @@ -84,8 +100,40 @@ public class EditProfileActivity extends AppCompatActivity { binding.cancelButton.setOnClickListener(v -> finish()); loadFromPrefs(); + setupGenderDropdown(); + setupLocationDropdown(); + setupBirthdayPicker(); binding.avatarRow.setOnClickListener(v -> showAvatarBottomSheet()); + + // 头像预览点击放大 + binding.avatarPreview.setOnClickListener(v -> { + AvatarViewerDialog dialog = AvatarViewerDialog.create(this); + + // 优先使用selectedAvatarUri(当前选择但未保存的头像) + if (selectedAvatarUri != null) { + dialog.setAvatarUri(selectedAvatarUri); + } else { + // 如果没有选择新头像,从SharedPreferences读取 + String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null); + int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0); + + if (!TextUtils.isEmpty(avatarUri)) { + dialog.setAvatarUri(Uri.parse(avatarUri)); + } else if (avatarRes != 0) { + dialog.setAvatarResId(avatarRes); + } else { + // 如果都没有,尝试从ImageView获取Drawable + Drawable drawable = binding.avatarPreview.getDrawable(); + if (drawable != null) { + dialog.setAvatarDrawable(drawable); + } else { + dialog.setAvatarResId(R.drawable.ic_account_circle_24); + } + } + } + dialog.show(); + }); binding.saveButton.setOnClickListener(v -> { String name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : ""; @@ -121,9 +169,12 @@ public class EditProfileActivity extends AppCompatActivity { } } + // 生日已经在日期选择器中实时保存,这里只需要确保同步 + // 如果输入框为空,则清除 if (TextUtils.isEmpty(birthday)) { getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_BIRTHDAY).apply(); } else { + // 确保使用输入框中的最新值 getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIRTHDAY, birthday).apply(); } @@ -139,6 +190,8 @@ public class EditProfileActivity extends AppCompatActivity { getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_LOCATION, location).apply(); } + // 设置结果,通知ProfileActivity需要刷新 + setResult(RESULT_OK); finish(); }); } @@ -200,6 +253,11 @@ public class EditProfileActivity extends AppCompatActivity { if (!TextUtils.isEmpty(avatarUri)) { selectedAvatarUri = Uri.parse(avatarUri); Glide.with(this).load(selectedAvatarUri).circleCrop().into(binding.avatarPreview); + } else { + int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0); + if (avatarRes != 0) { + Glide.with(this).load(avatarRes).circleCrop().into(binding.avatarPreview); + } } } @@ -250,4 +308,254 @@ public class EditProfileActivity extends AppCompatActivity { return null; } } + + private void setupGenderDropdown() { + String[] genders = {"男", "女", "保密"}; + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, genders); + + binding.inputGender.setOnClickListener(v -> { + android.widget.PopupMenu popupMenu = new android.widget.PopupMenu(this, binding.inputGender); + for (int i = 0; i < genders.length; i++) { + popupMenu.getMenu().add(android.view.Menu.NONE, i, i, genders[i]); + } + popupMenu.setOnMenuItemClickListener(item -> { + String selectedGender = genders[item.getItemId()]; + binding.inputGender.setText(selectedGender); + return true; + }); + popupMenu.show(); + }); + } + + private void setupLocationDropdown() { + binding.inputLocation.setOnClickListener(v -> showLocationPicker()); + } + + private void showLocationPicker() { + BottomSheetDialog dialog = new BottomSheetDialog(this); + View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_location_picker_spinner, null); + dialog.setContentView(view); + + Spinner provinceSpinner = view.findViewById(R.id.provinceSpinner); + Spinner citySpinner = view.findViewById(R.id.citySpinner); + TextView confirmButton = view.findViewById(R.id.confirmButton); + TextView cancelButton = view.findViewById(R.id.cancelButton); + + // 加载地址数据 + LocationDataManager locationManager = LocationDataManager.getInstance(); + locationManager.loadData(this); + + // 解析当前地址 + String currentLocation = binding.inputLocation.getText() != null ? binding.inputLocation.getText().toString() : ""; + String[] parsed = locationManager.parseLocation(currentLocation); + String currentProvince = parsed[0]; + String currentCity = parsed[1]; + + // 初始化省份Spinner + List provinceNames = locationManager.getProvinceNames(); + ArrayAdapter provinceAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, provinceNames); + provinceAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + provinceSpinner.setAdapter(provinceAdapter); + + // 设置当前选中的省份 + if (!TextUtils.isEmpty(currentProvince)) { + int provinceIndex = locationManager.findProvinceIndex(currentProvince); + if (provinceIndex >= 0 && provinceIndex < provinceNames.size()) { + provinceSpinner.setSelection(provinceIndex); + } + } + + // 初始化城市Spinner + final String[] selectedProvince = {currentProvince}; + final String[] selectedCity = {currentCity}; + + updateCitySpinner(citySpinner, locationManager, selectedProvince[0], selectedCity[0]); + + // 省份选择监听 + provinceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + selectedProvince[0] = provinceNames.get(position); + selectedCity[0] = ""; + updateCitySpinner(citySpinner, locationManager, selectedProvince[0], ""); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + + // 城市选择监听 + citySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + List cityNames = locationManager.getCityNames(selectedProvince[0]); + if (position >= 0 && position < cityNames.size()) { + selectedCity[0] = cityNames.get(position); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + + confirmButton.setOnClickListener(v -> { + String location = locationManager.formatLocation(selectedProvince[0], selectedCity[0]); + binding.inputLocation.setText(location); + dialog.dismiss(); + }); + + cancelButton.setOnClickListener(v -> dialog.dismiss()); + + dialog.show(); + } + + private void updateCitySpinner(Spinner citySpinner, LocationDataManager locationManager, String province, String selectedCity) { + List cityNames = locationManager.getCityNames(province); + ArrayAdapter cityAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, cityNames); + cityAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + citySpinner.setAdapter(cityAdapter); + + if (!TextUtils.isEmpty(selectedCity) && cityNames.contains(selectedCity)) { + int cityIndex = locationManager.findCityIndex(province, selectedCity); + if (cityIndex >= 0 && cityIndex < cityNames.size()) { + citySpinner.setSelection(cityIndex); + } + } else if (!cityNames.isEmpty()) { + citySpinner.setSelection(0); + } + } + + private void setupBirthdayPicker() { + binding.inputBirthday.setOnClickListener(v -> showDatePicker()); + } + + private void showDatePicker() { + BottomSheetDialog dialog = new BottomSheetDialog(this); + View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_date_picker, null); + dialog.setContentView(view); + + NumberPicker yearPicker = view.findViewById(R.id.yearPicker); + NumberPicker monthPicker = view.findViewById(R.id.monthPicker); + NumberPicker dayPicker = view.findViewById(R.id.dayPicker); + TextView confirmButton = view.findViewById(R.id.confirmButton); + TextView cancelButton = view.findViewById(R.id.cancelButton); + + // 获取当前日期 + Calendar today = Calendar.getInstance(); + int currentYear = today.get(Calendar.YEAR); + int currentMonth = today.get(Calendar.MONTH) + 1; // Calendar.MONTH 从0开始 + int currentDay = today.get(Calendar.DAY_OF_MONTH); + + // 解析当前生日 + String currentBirthday = binding.inputBirthday.getText() != null ? binding.inputBirthday.getText().toString() : ""; + final int[] selectedYear = {currentYear - 25}; // 默认25岁 + final int[] selectedMonth = {currentMonth}; + final int[] selectedDay = {currentDay}; + + if (!TextUtils.isEmpty(currentBirthday)) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + java.util.Date date = sdf.parse(currentBirthday); + if (date != null) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + selectedYear[0] = cal.get(Calendar.YEAR); + selectedMonth[0] = cal.get(Calendar.MONTH) + 1; + selectedDay[0] = cal.get(Calendar.DAY_OF_MONTH); + } + } catch (Exception e) { + // 解析失败,使用默认值 + } + } + + // 设置年份范围:100年前到今天 + int minYear = currentYear - 100; + int maxYear = currentYear; + yearPicker.setMinValue(minYear); + yearPicker.setMaxValue(maxYear); + yearPicker.setValue(selectedYear[0]); + yearPicker.setWrapSelectorWheel(false); + + // 设置月份范围:1-12 + monthPicker.setMinValue(1); + monthPicker.setMaxValue(12); + monthPicker.setValue(selectedMonth[0]); + monthPicker.setWrapSelectorWheel(false); + monthPicker.setDisplayedValues(new String[]{"1月", "2月", "3月", "4月", "5月", "6月", + "7月", "8月", "9月", "10月", "11月", "12月"}); + + // 更新日期选择器的最大值 + updateDayPicker(dayPicker, selectedYear[0], selectedMonth[0], selectedDay[0]); + + // 年份和月份变化时,更新日期选择器 + yearPicker.setOnValueChangedListener((picker, oldVal, newVal) -> { + selectedYear[0] = newVal; + int maxDay = getDaysInMonth(selectedYear[0], selectedMonth[0]); + if (selectedDay[0] > maxDay) { + selectedDay[0] = maxDay; + } + updateDayPicker(dayPicker, selectedYear[0], selectedMonth[0], selectedDay[0]); + }); + + monthPicker.setOnValueChangedListener((picker, oldVal, newVal) -> { + selectedMonth[0] = newVal; + int maxDay = getDaysInMonth(selectedYear[0], selectedMonth[0]); + if (selectedDay[0] > maxDay) { + selectedDay[0] = maxDay; + } + updateDayPicker(dayPicker, selectedYear[0], selectedMonth[0], selectedDay[0]); + }); + + // 确认按钮 + confirmButton.setOnClickListener(v -> { + int year = yearPicker.getValue(); + int month = monthPicker.getValue(); + int day = dayPicker.getValue(); + + // 格式化日期 + Calendar selectedDate = Calendar.getInstance(); + selectedDate.set(year, month - 1, day); // Calendar.MONTH 从0开始 + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + String formattedDate = sdf.format(selectedDate.getTime()); + + // 更新输入框显示 + binding.inputBirthday.setText(formattedDate); + + // 立即保存到SharedPreferences + getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + .edit() + .putString(KEY_BIRTHDAY, formattedDate) + .apply(); + + dialog.dismiss(); + }); + + // 取消按钮 + cancelButton.setOnClickListener(v -> dialog.dismiss()); + + dialog.show(); + } + + private void updateDayPicker(NumberPicker dayPicker, int year, int month, int selectedDay) { + int maxDay = getDaysInMonth(year, month); + dayPicker.setMinValue(1); + dayPicker.setMaxValue(maxDay); + dayPicker.setWrapSelectorWheel(false); + + // 确保选中的日期不超过最大值 + if (selectedDay > maxDay) { + selectedDay = maxDay; + } + dayPicker.setValue(selectedDay); + } + + private int getDaysInMonth(int year, int month) { + Calendar cal = Calendar.getInstance(); + cal.set(year, month - 1, 1); // Calendar.MONTH 从0开始 + return cal.getActualMaximum(Calendar.DAY_OF_MONTH); + } } + diff --git a/android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java new file mode 100644 index 00000000..2b7e09e1 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java @@ -0,0 +1,61 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityFindGameBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class FindGameActivity extends AppCompatActivity { + + private ActivityFindGameBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, FindGameActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityFindGameBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java index 8bd453de..ec8bd96f 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java @@ -1,10 +1,12 @@ package com.example.livestreaming; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.animation.ValueAnimator; import android.os.Handler; import android.os.Looper; +import android.text.TextUtils; import android.view.HapticFeedbackConstants; import android.view.View; import android.view.animation.LinearInterpolator; @@ -70,12 +72,18 @@ public class FishPondActivity extends AppCompatActivity { setupOrbitUserInteractions(); setupCenterRefresh(); + setupQuickActions(); + loadCurrentUserInfo(); binding.orbitContainer.post(this::updateOrbitLayout); refreshUsers(); BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; bottomNavigation.setSelectedItemId(R.id.nav_friends); + + // 更新未读消息徽章 + UnreadMessageManager.updateBadge(bottomNavigation); + bottomNavigation.setOnItemSelectedListener(item -> { int id = item.getItemId(); if (id == R.id.nav_home) { @@ -116,6 +124,54 @@ public class FishPondActivity extends AppCompatActivity { }); } + private void setupQuickActions() { + if (binding == null) return; + + // 语音匹配按钮 + binding.actionLeft.setOnClickListener(v -> { + VoiceMatchActivity.start(this); + }); + + // 心动信号按钮 + binding.actionRight.setOnClickListener(v -> { + HeartbeatSignalActivity.start(this); + }); + + // 获取GridLayout中的卡片并设置点击事件 + android.widget.GridLayout grid = binding.grid; + if (grid != null) { + int childCount = grid.getChildCount(); + for (int i = 0; i < childCount && i < 6; i++) { + View card = grid.getChildAt(i); + if (card != null) { + final int index = i; + card.setOnClickListener(v -> { + switch (index) { + case 0: // 在线处对象 + OnlineDatingActivity.start(this); + break; + case 1: // 找人玩游戏 + FindGameActivity.start(this); + break; + case 2: // 一起KTV + KTVTogetherActivity.start(this); + break; + case 3: // 你画我猜 + DrawGuessActivity.start(this); + break; + case 4: // 和平精英 + PeaceEliteActivity.start(this); + break; + case 5: // 桌子游 + TableGamesActivity.start(this); + break; + } + }); + } + } + } + } + private void updateOrbitLayout() { if (binding == null) return; @@ -227,6 +283,42 @@ public class FishPondActivity extends AppCompatActivity { uiHandler.removeCallbacks(pulseRunnable); } + private void loadCurrentUserInfo() { + if (binding == null) return; + + SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE); + String name = prefs.getString("profile_name", "爱你"); + String location = prefs.getString("profile_location", "广西"); + String gender = prefs.getString("profile_gender", ""); + + // 设置右侧名称 + binding.rightName.setText(name); + + // 设置右侧信息:性别 | 地址 | 在线 + String genderText = ""; + if (!TextUtils.isEmpty(gender)) { + if (gender.contains("男")) { + genderText = "真诚"; + } else if (gender.contains("女")) { + genderText = "真诚"; + } else { + genderText = "真诚"; + } + } else { + genderText = "真诚"; + } + + // 处理地址格式:如果是"省份-城市"格式,转换为"省份·城市" + String locationText = location; + if (!TextUtils.isEmpty(location) && location.contains("-")) { + locationText = location.replace("-", "·"); + } else if (TextUtils.isEmpty(location)) { + locationText = "南宁"; + } + + binding.rightInfo.setText(genderText + " | " + locationText + " | 在线"); + } + private void refreshUsers() { if (binding == null) return; @@ -330,9 +422,15 @@ public class FishPondActivity extends AppCompatActivity { protected void onResume() { super.onResume(); if (binding != null) { - binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_friends); + BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation; + bottomNav.setSelectedItemId(R.id.nav_friends); + // 更新未读消息徽章 + UnreadMessageManager.updateBadge(bottomNav); } + // 刷新当前用户信息(可能从编辑资料页面返回) + loadCurrentUserInfo(); + // ensure radius computed before starting if (binding != null) { binding.orbitContainer.post(() -> { diff --git a/android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java b/android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java new file mode 100644 index 00000000..fbe1e118 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java @@ -0,0 +1,62 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityHeartbeatSignalBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class HeartbeatSignalActivity extends AppCompatActivity { + + private ActivityHeartbeatSignalBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, HeartbeatSignalActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityHeartbeatSignalBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java b/android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java new file mode 100644 index 00000000..1765b46b --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java @@ -0,0 +1,61 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityKtvTogetherBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class KTVTogetherActivity extends AppCompatActivity { + + private ActivityKtvTogetherBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, KTVTogetherActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityKtvTogetherBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/LocationDataManager.java b/android-app/app/src/main/java/com/example/livestreaming/LocationDataManager.java new file mode 100644 index 00000000..353f24b8 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/LocationDataManager.java @@ -0,0 +1,142 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LocationDataManager { + private static LocationDataManager instance; + private List provinces; + private Map provinceMap; + + private LocationDataManager() { + provinces = new ArrayList<>(); + provinceMap = new HashMap<>(); + } + + public static synchronized LocationDataManager getInstance() { + if (instance == null) { + instance = new LocationDataManager(); + } + return instance; + } + + public void loadData(Context context) { + if (provinces != null && !provinces.isEmpty()) { + return; // 已经加载过 + } + + try { + InputStream is = context.getAssets().open("location_data.json"); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + + String json = new String(buffer, "UTF-8"); + JSONObject jsonObject = new JSONObject(json); + JSONArray provincesArray = jsonObject.getJSONArray("provinces"); + + provinces.clear(); + provinceMap.clear(); + + for (int i = 0; i < provincesArray.length(); i++) { + JSONObject provinceObj = provincesArray.getJSONObject(i); + Province province = new Province(); + province.name = provinceObj.getString("name"); + + JSONArray citiesArray = provinceObj.getJSONArray("cities"); + province.cities = new ArrayList<>(); + for (int j = 0; j < citiesArray.length(); j++) { + JSONObject cityObj = citiesArray.getJSONObject(j); + City city = new City(); + city.name = cityObj.getString("name"); + province.cities.add(city); + } + + provinces.add(province); + provinceMap.put(province.name, province); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public List getProvinceNames() { + List names = new ArrayList<>(); + for (Province province : provinces) { + names.add(province.name); + } + return names; + } + + public List getCityNames(String provinceName) { + List names = new ArrayList<>(); + Province province = provinceMap.get(provinceName); + if (province != null && province.cities != null) { + for (City city : province.cities) { + names.add(city.name); + } + } + return names; + } + + public String formatLocation(String province, String city) { + if (TextUtils.isEmpty(province)) { + return ""; + } + if (TextUtils.isEmpty(city)) { + return province; + } + return province + "-" + city; + } + + public String[] parseLocation(String location) { + if (TextUtils.isEmpty(location)) { + return new String[]{"", ""}; + } + if (location.contains("-")) { + String[] parts = location.split("-", 2); + return new String[]{parts[0], parts.length > 1 ? parts[1] : ""}; + } + return new String[]{location, ""}; + } + + public int findProvinceIndex(String provinceName) { + for (int i = 0; i < provinces.size(); i++) { + if (provinces.get(i).name.equals(provinceName)) { + return i; + } + } + return 0; + } + + public int findCityIndex(String provinceName, String cityName) { + Province province = provinceMap.get(provinceName); + if (province != null && province.cities != null) { + for (int i = 0; i < province.cities.size(); i++) { + if (province.cities.get(i).name.equals(cityName)) { + return i; + } + } + } + return 0; + } + + static class Province { + String name; + List cities; + } + + static class City { + String name; + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index da676ef5..22f08039 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -1,14 +1,21 @@ package com.example.livestreaming; +import android.Manifest; import android.content.res.AssetManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.speech.RecognitionListener; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; import android.view.View; import android.widget.TextView; import android.widget.Toast; @@ -16,6 +23,8 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.recyclerview.widget.LinearLayoutManager; @@ -56,6 +65,11 @@ public class MainActivity extends AppCompatActivity { private boolean isFetching; private long lastFetchMs; + private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200; + private SpeechRecognizer speechRecognizer; + private Intent speechRecognizerIntent; + private boolean isListening = false; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -66,6 +80,29 @@ public class MainActivity extends AppCompatActivity { setupRecyclerView(); setupUI(); loadAvatarFromPrefs(); + setupSpeechRecognizer(); + + // 初始化未读消息数量(演示数据) + if (UnreadMessageManager.getUnreadCount(this) == 0) { + // 从消息列表计算总未读数量 + UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount()); + } + + // 清除默认选中状态,让所有标签页初始显示为未选中样式 + if (binding != null && binding.topTabs != null) { + // 在布局完成后清除默认选中状态 + binding.topTabs.post(() -> { + // 清除所有选中状态 + for (int i = 0; i < binding.topTabs.getTabCount(); i++) { + TabLayout.Tab tab = binding.topTabs.getTabAt(i); + if (tab != null && tab.isSelected()) { + // 取消选中,但不触发监听器 + binding.topTabs.selectTab(null, false); + break; + } + } + }); + } // 异步加载资源文件,避免阻塞主线程 loadCoverAssetsAsync(); @@ -91,7 +128,7 @@ public class MainActivity extends AppCompatActivity { items.add(new DrawerCardItem(DrawerCardItem.ACTION_PROFILE, "个人主页", "资料、作品、粉丝", R.drawable.ic_person_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_MESSAGES, "消息", "私信、互动通知", R.drawable.ic_chat_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_MY_FRIENDS, "我的好友", "通讯录与挚友", R.drawable.ic_people_24)); - items.add(new DrawerCardItem(DrawerCardItem.ACTION_FISH_POND, "鱼塘", "附近与社交圈", R.drawable.ic_globe_24)); + items.add(new DrawerCardItem(DrawerCardItem.ACTION_FISH_POND, "缘池", "附近与社交圈", R.drawable.ic_people_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_FOLLOWING, "我的关注", "你关注的主播", R.drawable.ic_people_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_FANS, "粉丝", "关注你的人", R.drawable.ic_people_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_LIKES, "获赞", "收到的点赞", R.drawable.ic_heart_24)); @@ -204,12 +241,20 @@ public class MainActivity extends AppCompatActivity { @Override public void onTabSelected(TabLayout.Tab tab) { if (tab == null) return; + // 用户点击后,显示选中样式(显示指示器,改变文字颜色) + binding.topTabs.setSelectedTabIndicatorHeight(2); // 显示指示器 + // 更新布局属性以显示选中样式 + binding.topTabs.setTabTextColors( + getResources().getColor(android.R.color.darker_gray, null), // 未选中颜色 + getResources().getColor(R.color.purple_500, null) // 选中颜色 + ); CharSequence title = tab.getText(); TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : ""); } @Override public void onTabUnselected(TabLayout.Tab tab) { + // 取消选中时,恢复默认样式 } @Override @@ -271,8 +316,15 @@ public class MainActivity extends AppCompatActivity { // 设置添加直播按钮点击事件 binding.fabAddLive.setOnClickListener(v -> showCreateRoomDialog()); + // 设置搜索框文本监听和图标切换 + setupSearchBox(); + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; bottomNavigation.setSelectedItemId(R.id.nav_home); + + // 更新未读消息徽章 + UnreadMessageManager.updateBadge(bottomNavigation); + bottomNavigation.setOnItemSelectedListener(item -> { int id = item.getItemId(); if (id == R.id.nav_home) { @@ -302,6 +354,245 @@ public class MainActivity extends AppCompatActivity { }); } + private void setupSearchBox() { + // 监听搜索框文本变化 + binding.searchEdit.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // 根据文本是否为空切换图标 + String text = s != null ? s.toString().trim() : ""; + if (text.isEmpty()) { + // 文本为空,显示麦克风图标 + binding.micIcon.setImageResource(R.drawable.ic_mic_24); + binding.micIcon.setContentDescription("mic"); + } else { + // 文本不为空,显示搜索图标 + binding.micIcon.setImageResource(R.drawable.ic_search_24); + binding.micIcon.setContentDescription("search"); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + // 设置麦克风/搜索图标点击事件 + binding.micIcon.setOnClickListener(v -> { + String searchText = binding.searchEdit.getText() != null ? + binding.searchEdit.getText().toString().trim() : ""; + + if (searchText.isEmpty()) { + // 如果文本为空,启动语音识别 + startVoiceRecognition(); + } else { + // 如果文本不为空,执行搜索 + // 跳转到搜索页面并传递搜索关键词 + SearchActivity.start(MainActivity.this, searchText); + } + }); + } + + private void setupSpeechRecognizer() { + // 检查设备是否支持语音识别 + if (!SpeechRecognizer.isRecognitionAvailable(this)) { + return; + } + + speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this); + speechRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN"); // 设置为中文 + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1); + + speechRecognizer.setRecognitionListener(new RecognitionListener() { + @Override + public void onReadyForSpeech(Bundle params) { + isListening = true; + // 可以在这里显示"正在聆听..."的提示 + binding.micIcon.setAlpha(0.5f); + } + + @Override + public void onBeginningOfSpeech() { + // 开始说话 + } + + @Override + public void onRmsChanged(float rmsdB) { + // 音量变化,可以用来显示音量动画 + } + + @Override + public void onBufferReceived(byte[] buffer) { + // 接收音频数据 + } + + @Override + public void onEndOfSpeech() { + isListening = false; + binding.micIcon.setAlpha(1.0f); + } + + @Override + public void onError(int error) { + isListening = false; + binding.micIcon.setAlpha(1.0f); + + String errorMessage; + switch (error) { + case SpeechRecognizer.ERROR_AUDIO: + errorMessage = "音频错误"; + break; + case SpeechRecognizer.ERROR_CLIENT: + errorMessage = "客户端错误"; + break; + case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: + errorMessage = "权限不足"; + requestAudioPermission(); + return; + case SpeechRecognizer.ERROR_NETWORK: + errorMessage = "网络错误"; + break; + case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: + errorMessage = "网络超时"; + break; + case SpeechRecognizer.ERROR_NO_MATCH: + errorMessage = "未识别到语音"; + break; + case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: + errorMessage = "识别器忙碌"; + break; + case SpeechRecognizer.ERROR_SERVER: + errorMessage = "服务器错误"; + break; + case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + errorMessage = "未检测到语音"; + break; + default: + errorMessage = "未知错误"; + break; + } + + // 只在非用户取消的情况下显示错误提示 + if (error != SpeechRecognizer.ERROR_CLIENT) { + Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onResults(Bundle results) { + isListening = false; + binding.micIcon.setAlpha(1.0f); + + ArrayList matches = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + if (matches != null && !matches.isEmpty()) { + String spokenText = matches.get(0); + // 将识别结果填充到搜索框 + binding.searchEdit.setText(spokenText); + binding.searchEdit.setSelection(spokenText.length()); + } + } + + @Override + public void onPartialResults(Bundle partialResults) { + // 部分结果,可以实时显示 + ArrayList matches = partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + if (matches != null && !matches.isEmpty()) { + String partialText = matches.get(0); + // 可以在这里实时更新搜索框(可选) + // binding.searchEdit.setText(partialText); + } + } + + @Override + public void onEvent(int eventType, Bundle params) { + // 事件回调 + } + }); + } + + private void startVoiceRecognition() { + // 检查权限 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + requestAudioPermission(); + return; + } + + // 检查是否正在监听 + if (isListening) { + stopVoiceRecognition(); + return; + } + + // 检查SpeechRecognizer是否可用 + if (speechRecognizer == null || speechRecognizerIntent == null) { + Toast.makeText(this, "语音识别功能不可用", Toast.LENGTH_SHORT).show(); + return; + } + + try { + speechRecognizer.startListening(speechRecognizerIntent); + } catch (Exception e) { + Toast.makeText(this, "启动语音识别失败", Toast.LENGTH_SHORT).show(); + } + } + + private void stopVoiceRecognition() { + if (speechRecognizer != null && isListening) { + speechRecognizer.stopListening(); + isListening = false; + binding.micIcon.setAlpha(1.0f); + } + } + + private void requestAudioPermission() { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) { + new AlertDialog.Builder(this) + .setTitle("需要麦克风权限") + .setMessage("语音搜索需要访问麦克风权限,请在设置中允许") + .setPositiveButton("确定", (dialog, which) -> { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, + REQUEST_RECORD_AUDIO_PERMISSION); + }) + .setNegativeButton("取消", null) + .show(); + } else { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, + REQUEST_RECORD_AUDIO_PERMISSION); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // 权限已授予,可以开始语音识别 + startVoiceRecognition(); + } else { + Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show(); + } + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (speechRecognizer != null) { + speechRecognizer.destroy(); + speechRecognizer = null; + } + } + private void loadAvatarFromPrefs() { try { String avatarUri = getSharedPreferences("profile_prefs", MODE_PRIVATE) @@ -340,8 +631,31 @@ public class MainActivity extends AppCompatActivity { BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; bottomNavigation.setSelectedItemId(R.id.nav_home); loadAvatarFromPrefs(); + // 更新未读消息徽章 + UnreadMessageManager.updateBadge(bottomNavigation); } } + + /** + * 计算总未读消息数量(从演示数据中计算) + */ + private int calculateTotalUnreadCount() { + // 模拟从消息列表计算总未读数量 + // 这里使用 MessagesActivity 中的演示数据 + int total = 0; + total += 2; // 系统通知 + total += 5; // 附近的人 + total += 19; // 直播间群聊 + total += 1; // 客服 + return total; + } + + @Override + protected void onPause() { + super.onPause(); + // 暂停时停止语音识别 + stopVoiceRecognition(); + } @Override protected void onStart() { diff --git a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java index 206ce2c8..7124c021 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java @@ -33,8 +33,11 @@ public class MessagesActivity extends AppCompatActivity { private int swipedPosition = RecyclerView.NO_POSITION; private RectF deleteButtonRect; + private RectF markReadButtonRect; private final Paint deleteBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint deleteTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint markReadBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint markReadTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private int lastSwipePosition = RecyclerView.NO_POSITION; private float lastSwipeDx; @@ -89,14 +92,35 @@ public class MessagesActivity extends AppCompatActivity { } private void setupConversationList() { + // 删除按钮的背景颜色(红色) deleteBackgroundPaint.setColor(0xFFE53935); deleteTextPaint.setColor(Color.WHITE); deleteTextPaint.setTextAlign(Paint.Align.CENTER); deleteTextPaint.setTextSize(sp(14)); + + // 标记已读按钮的背景颜色(蓝色) + markReadBackgroundPaint.setColor(0xFF2196F3); + markReadTextPaint.setColor(Color.WHITE); + markReadTextPaint.setTextAlign(Paint.Align.CENTER); + markReadTextPaint.setTextSize(sp(14)); conversationsAdapter = new ConversationsAdapter(item -> { if (item == null) return; - ConversationActivity.start(this, item.getId(), item.getTitle()); + // 启动会话页面,传递未读数量 + Intent intent = new Intent(this, ConversationActivity.class); + intent.putExtra("extra_conversation_id", item.getId()); + intent.putExtra("extra_conversation_title", item.getTitle()); + intent.putExtra("extra_unread_count", item.getUnreadCount()); + startActivity(intent); + + // 用户点击会话时,减少该会话的未读数量 + if (item.getUnreadCount() > 0) { + // 更新该会话的未读数量为0(在实际应用中,这里应该更新数据源) + // 然后更新总未读数量 + UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount()); + // 更新列表中的未读数量显示 + updateConversationUnreadCount(item.getId(), 0); + } }); binding.conversationsRecyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.conversationsRecyclerView.setAdapter(conversationsAdapter); @@ -109,7 +133,9 @@ public class MessagesActivity extends AppCompatActivity { } private void attachSwipeToDelete(RecyclerView recyclerView) { - final float actionWidth = dp(96); + // 两个按钮的总宽度:删除按钮 + 标记已读按钮 + final float buttonWidth = dp(96); + final float actionWidth = buttonWidth * 2; // 两个按钮并排显示 touchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override @@ -140,6 +166,7 @@ public class MessagesActivity extends AppCompatActivity { int old = swipedPosition; swipedPosition = RecyclerView.NO_POSITION; deleteButtonRect = null; + markReadButtonRect = null; conversationsAdapter.notifyItemChanged(old); } swipedPosition = pos; @@ -148,6 +175,7 @@ public class MessagesActivity extends AppCompatActivity { if (swipedPosition == pos) { swipedPosition = RecyclerView.NO_POSITION; deleteButtonRect = null; + markReadButtonRect = null; } viewHolder.itemView.setTranslationX(0f); } @@ -207,20 +235,37 @@ public class MessagesActivity extends AppCompatActivity { } View itemView = viewHolder.itemView; - float left; float top = itemView.getTop(); float bottom = itemView.getBottom(); float right = itemView.getRight(); - left = right + clampedDx; + float left = right + clampedDx; - deleteButtonRect = new RectF(left, top, right, bottom); + // 绘制两个按钮:标记已读(左侧)和删除(右侧) + final float buttonWidth = dp(96); + + // 标记已读按钮(左侧,蓝色) + float markReadLeft = left; + float markReadRight = left + buttonWidth; + markReadButtonRect = new RectF(markReadLeft, top, markReadRight, bottom); + c.drawRect(markReadButtonRect, markReadBackgroundPaint); + + float markReadCenterX = markReadButtonRect.centerX(); + float markReadCenterY = markReadButtonRect.centerY(); + Paint.FontMetrics markReadFm = markReadTextPaint.getFontMetrics(); + float markReadTextY = markReadCenterY - (markReadFm.ascent + markReadFm.descent) / 2f; + c.drawText("标记已读", markReadCenterX, markReadTextY, markReadTextPaint); + + // 删除按钮(右侧,红色) + float deleteLeft = markReadRight; + float deleteRight = right; + deleteButtonRect = new RectF(deleteLeft, top, deleteRight, bottom); c.drawRect(deleteButtonRect, deleteBackgroundPaint); - - float centerX = deleteButtonRect.centerX(); - float centerY = deleteButtonRect.centerY(); - Paint.FontMetrics fm = deleteTextPaint.getFontMetrics(); - float textY = centerY - (fm.ascent + fm.descent) / 2f; - c.drawText("删除", centerX, textY, deleteTextPaint); + + float deleteCenterX = deleteButtonRect.centerX(); + float deleteCenterY = deleteButtonRect.centerY(); + Paint.FontMetrics deleteFm = deleteTextPaint.getFontMetrics(); + float deleteTextY = deleteCenterY - (deleteFm.ascent + deleteFm.descent) / 2f; + c.drawText("删除", deleteCenterX, deleteTextY, deleteTextPaint); super.onChildDraw(c, recyclerView, viewHolder, clampedDx, dY, actionState, isCurrentlyActive); } @@ -229,7 +274,7 @@ public class MessagesActivity extends AppCompatActivity { new ItemTouchHelper(callback).attachToRecyclerView(recyclerView); recyclerView.setOnTouchListener((v, event) -> { - if (swipedPosition == RecyclerView.NO_POSITION || deleteButtonRect == null) return false; + if (swipedPosition == RecyclerView.NO_POSITION || (deleteButtonRect == null && markReadButtonRect == null)) return false; int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { @@ -251,10 +296,20 @@ public class MessagesActivity extends AppCompatActivity { if (action == MotionEvent.ACTION_UP) { if (!touchIsClick) return false; - boolean hit = deleteButtonRect.contains(event.getX(), event.getY()); int pos = swipedPosition; + float x = event.getX(); + float y = event.getY(); + + // 检查点击的是哪个按钮 + boolean hitMarkRead = markReadButtonRect != null && markReadButtonRect.contains(x, y); + boolean hitDelete = deleteButtonRect != null && deleteButtonRect.contains(x, y); + recoverSwipedItem(); - if (hit) { + + if (hitMarkRead) { + markAsReadAt(pos); + return true; + } else if (hitDelete) { deleteConversationAt(pos); return true; } @@ -267,17 +322,56 @@ public class MessagesActivity extends AppCompatActivity { int pos = swipedPosition; swipedPosition = RecyclerView.NO_POSITION; deleteButtonRect = null; + markReadButtonRect = null; if (pos != RecyclerView.NO_POSITION && conversationsAdapter != null) { conversationsAdapter.notifyItemChanged(pos); } } + /** + * 标记会话为已读 + */ + private void markAsReadAt(int position) { + if (position < 0 || position >= conversations.size()) return; + + ConversationItem item = conversations.get(position); + if (item == null) return; + + // 获取该会话的未读数量 + int unreadCount = item.getUnreadCount(); + + // 如果已经有未读消息,才需要更新 + if (unreadCount > 0) { + // 将该会话的未读数量设为0 + // updateConversationUnreadCount 方法会自动重新计算总未读数量并更新徽章 + updateConversationUnreadCount(item.getId(), 0); + } + } + + /** + * 删除会话 + */ private void deleteConversationAt(int position) { if (position < 0 || position >= conversations.size()) return; + + // 获取要删除的会话的未读数量 + ConversationItem itemToDelete = conversations.get(position); + int unreadCountToRemove = itemToDelete != null ? itemToDelete.getUnreadCount() : 0; + + // 删除会话 conversations.remove(position); if (conversationsAdapter != null) { conversationsAdapter.submitList(new ArrayList<>(conversations)); } + + // 更新总未读数量:减少被删除会话的未读数量 + if (unreadCountToRemove > 0) { + UnreadMessageManager.decrementUnreadCount(this, unreadCountToRemove); + // 更新底部导航栏徽章 + if (binding != null) { + UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation); + } + } } private float dp(float value) { @@ -304,7 +398,65 @@ public class MessagesActivity extends AppCompatActivity { protected void onResume() { super.onResume(); if (binding != null) { - binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_messages); + BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation; + bottomNav.setSelectedItemId(R.id.nav_messages); + + // 用户进入消息页面时,计算并更新总未读数量 + int totalUnread = calculateTotalUnreadCount(); + UnreadMessageManager.setUnreadCount(this, totalUnread); + UnreadMessageManager.updateBadge(bottomNav); + + // 刷新列表以更新未读数量显示 + if (conversationsAdapter != null) { + conversationsAdapter.submitList(new ArrayList<>(conversations)); + } + } + } + + /** + * 计算总未读消息数量 + */ + private int calculateTotalUnreadCount() { + int total = 0; + for (ConversationItem item : conversations) { + if (item != null) { + total += item.getUnreadCount(); + } + } + return total; + } + + /** + * 更新会话的未读数量 + */ + private void updateConversationUnreadCount(String conversationId, int newUnreadCount) { + for (int i = 0; i < conversations.size(); i++) { + ConversationItem item = conversations.get(i); + if (item != null && item.getId().equals(conversationId)) { + // 创建新的 ConversationItem,更新未读数量 + conversations.set(i, new ConversationItem( + item.getId(), + item.getTitle(), + item.getLastMessage(), + item.getTimeText(), + newUnreadCount, + item.isMuted() + )); + break; + } + } + + // 通知适配器更新列表显示 + if (conversationsAdapter != null) { + conversationsAdapter.submitList(new ArrayList<>(conversations)); + } + + // 更新总未读数量 + int totalUnread = calculateTotalUnreadCount(); + UnreadMessageManager.setUnreadCount(this, totalUnread); + // 更新底部导航栏徽章 + if (binding != null) { + UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation); } } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java index 8d77549d..8880c12f 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java @@ -5,7 +5,6 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; -import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; @@ -50,37 +49,25 @@ public class NearbyUsersAdapter extends ListAdapter { + if (user == null) return; + if (onUserClickListener != null) onUserClickListener.onUserClick(user); + }); + + // 整个item点击也可以触发添加 binding.getRoot().setOnClickListener(v -> { if (user == null) return; if (onUserClickListener != null) onUserClickListener.onUserClick(user); diff --git a/android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java b/android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java new file mode 100644 index 00000000..3ed05067 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java @@ -0,0 +1,61 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityOnlineDatingBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class OnlineDatingActivity extends AppCompatActivity { + + private ActivityOnlineDatingBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, OnlineDatingActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityOnlineDatingBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java new file mode 100644 index 00000000..77f4b6a1 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java @@ -0,0 +1,61 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityPeaceEliteBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class PeaceEliteActivity extends AppCompatActivity { + + private ActivityPeaceEliteBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, PeaceEliteActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityPeaceEliteBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java index 7ae09465..12322d9d 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.Intent; import android.content.ClipData; import android.content.ClipboardManager; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.net.Uri; import android.text.TextUtils; @@ -11,6 +12,9 @@ import android.view.View; import android.widget.EditText; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; + import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AlertDialog; @@ -18,6 +22,12 @@ import com.bumptech.glide.Glide; import com.example.livestreaming.databinding.ActivityProfileBinding; import com.google.android.material.bottomnavigation.BottomNavigationView; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + public class ProfileActivity extends AppCompatActivity { private ActivityProfileBinding binding; @@ -35,6 +45,8 @@ public class ProfileActivity extends AppCompatActivity { private static final String KEY_LOCATION = "profile_location"; private static final String BIO_HINT_TEXT = "填写个人签名更容易获得关注,点击此处添加"; + + private ActivityResultLauncher editProfileLauncher; public static void start(Context context) { Intent intent = new Intent(context, ProfileActivity.class); @@ -47,13 +59,31 @@ public class ProfileActivity extends AppCompatActivity { binding = ActivityProfileBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + // 注册编辑资料页面的结果监听 + editProfileLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + // 当从EditProfileActivity返回时,立即刷新所有数据 + loadProfileFromPrefs(); + loadAndDisplayTags(); + loadProfileInfo(); + } + ); + loadProfileFromPrefs(); + loadAndDisplayTags(); + loadProfileInfo(); setupEditableAreas(); + setupAvatarClick(); setupNavigationClicks(); setupProfileTabs(); BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; bottomNavigation.setSelectedItemId(R.id.nav_profile); + + // 更新未读消息徽章 + UnreadMessageManager.updateBadge(bottomNavigation); + bottomNavigation.setOnItemSelectedListener(item -> { int id = item.getItemId(); if (id == R.id.nav_home) { @@ -140,6 +170,32 @@ public class ProfileActivity extends AppCompatActivity { } + private void setupAvatarClick() { + binding.avatar.setOnClickListener(v -> { + AvatarViewerDialog dialog = AvatarViewerDialog.create(this); + + // 优先从SharedPreferences读取最新的头像信息(因为ImageView可能还在加载中) + String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null); + int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0); + + if (!TextUtils.isEmpty(avatarUri)) { + // 使用URI加载,确保能正确显示 + dialog.setAvatarUri(Uri.parse(avatarUri)); + } else if (avatarRes != 0) { + dialog.setAvatarResId(avatarRes); + } else { + // 如果都没有,尝试从ImageView获取Drawable + Drawable drawable = binding.avatar.getDrawable(); + if (drawable != null) { + dialog.setAvatarDrawable(drawable); + } else { + dialog.setAvatarResId(R.drawable.ic_account_circle_24); + } + } + dialog.show(); + }); + } + private void setupNavigationClicks() { binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现")); binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this)); @@ -161,13 +217,14 @@ public class ProfileActivity extends AppCompatActivity { binding.followers.setOnClickListener(v -> FansListActivity.start(this)); binding.likes.setOnClickListener(v -> LikesListActivity.start(this)); - binding.recordBtn.setOnClickListener(v -> Toast.makeText(this, "录制声音(待实现)", Toast.LENGTH_SHORT).show()); - binding.action1.setOnClickListener(v -> TabPlaceholderActivity.start(this, "公园勋章")); binding.action2.setOnClickListener(v -> WatchHistoryActivity.start(this)); binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class))); - binding.editProfile.setOnClickListener(v -> EditProfileActivity.start(this)); + binding.editProfile.setOnClickListener(v -> { + Intent intent = new Intent(this, EditProfileActivity.class); + editProfileLauncher.launch(intent); + }); binding.shareHome.setOnClickListener(v -> { // TabPlaceholderActivity.start(this, "分享主页"); String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : ""; @@ -209,14 +266,18 @@ public class ProfileActivity extends AppCompatActivity { binding.worksPublishBtn.setOnClickListener(v -> Toast.makeText(this, "发布功能待接入", Toast.LENGTH_SHORT).show()); binding.likedGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class))); binding.favGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class))); - binding.profileEditFromTab.setOnClickListener(v -> EditProfileActivity.start(this)); + binding.profileEditFromTab.setOnClickListener(v -> { + Intent intent = new Intent(this, EditProfileActivity.class); + editProfileLauncher.launch(intent); + }); } private void showTab(int index) { + // 标签页顺序:0-作品, 1-收藏, 2-赞过 binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE); - binding.tabLiked.setVisibility(index == 1 ? View.VISIBLE : View.GONE); - binding.tabFavorites.setVisibility(index == 2 ? View.VISIBLE : View.GONE); - binding.tabProfile.setVisibility(index == 3 ? View.VISIBLE : View.GONE); + binding.tabFavorites.setVisibility(index == 1 ? View.VISIBLE : View.GONE); + binding.tabLiked.setVisibility(index == 2 ? View.VISIBLE : View.GONE); + // "资料"标签页已移除 } private interface OnTextSaved { @@ -251,7 +312,153 @@ public class ProfileActivity extends AppCompatActivity { super.onResume(); if (binding != null) { loadProfileFromPrefs(); - binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_profile); + loadAndDisplayTags(); + loadProfileInfo(); + BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation; + bottomNav.setSelectedItemId(R.id.nav_profile); + // 更新未读消息徽章 + UnreadMessageManager.updateBadge(bottomNav); + } + } + + private void loadAndDisplayTags() { + String location = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_LOCATION, ""); + String gender = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_GENDER, ""); + String birthday = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_BIRTHDAY, ""); + + // 设置所在地标签 - 支持"省份-城市"格式 + if (!TextUtils.isEmpty(location)) { + // 将"省份-城市"格式转换为"省份·城市"显示 + String displayLocation = location.replace("-", "·"); + binding.tagLocation.setText("IP:" + displayLocation); + binding.tagLocation.setVisibility(View.VISIBLE); + } else { + binding.tagLocation.setText("IP:广西"); + binding.tagLocation.setVisibility(View.VISIBLE); + } + + // 设置性别标签 + if (!TextUtils.isEmpty(gender)) { + if (gender.contains("男")) { + binding.tagGender.setText("男"); + } else if (gender.contains("女")) { + binding.tagGender.setText("女"); + } else { + binding.tagGender.setText("H"); + } + binding.tagGender.setVisibility(View.VISIBLE); + } else { + binding.tagGender.setText("H"); + binding.tagGender.setVisibility(View.VISIBLE); + } + + // 计算并设置年龄标签 + if (!TextUtils.isEmpty(birthday)) { + int age = calculateAge(birthday); + if (age > 0) { + binding.tagAge.setText(age + "岁"); + binding.tagAge.setVisibility(View.VISIBLE); + } else { + binding.tagAge.setVisibility(View.GONE); + } + } else { + binding.tagAge.setVisibility(View.GONE); + } + + // 计算并设置星座标签 + if (!TextUtils.isEmpty(birthday)) { + String constellation = calculateConstellation(birthday); + if (!TextUtils.isEmpty(constellation)) { + binding.tagConstellation.setText(constellation); + binding.tagConstellation.setVisibility(View.VISIBLE); + } else { + binding.tagConstellation.setVisibility(View.GONE); + } + } else { + binding.tagConstellation.setVisibility(View.GONE); + } + } + + private void loadProfileInfo() { + String name = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, "爱你"); + String location = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_LOCATION, "广西"); + String bio = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_BIO, null); + + if (binding.profileInfoLine1 != null) { + binding.profileInfoLine1.setText("昵称:" + name); + } + + if (binding.profileInfoLine2 != null) { + String locationText = !TextUtils.isEmpty(location) ? location : "广西"; + binding.profileInfoLine2.setText("地区:" + locationText); + } + + if (binding.profileInfoLine3 != null) { + String bioText = (!TextUtils.isEmpty(bio) && !BIO_HINT_TEXT.equals(bio)) ? bio : "填写个人签名更容易获得关注"; + binding.profileInfoLine3.setText("签名:" + bioText); + } + } + + private int calculateAge(String birthdayStr) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + Date birthDate = sdf.parse(birthdayStr); + if (birthDate == null) return 0; + + Calendar birth = Calendar.getInstance(); + birth.setTime(birthDate); + Calendar now = Calendar.getInstance(); + + int age = now.get(Calendar.YEAR) - birth.get(Calendar.YEAR); + if (now.get(Calendar.DAY_OF_YEAR) < birth.get(Calendar.DAY_OF_YEAR)) { + age--; + } + return age; + } catch (ParseException e) { + return 0; + } + } + + private String calculateConstellation(String birthdayStr) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + Date birthDate = sdf.parse(birthdayStr); + if (birthDate == null) return ""; + + Calendar cal = Calendar.getInstance(); + cal.setTime(birthDate); + int month = cal.get(Calendar.MONTH) + 1; // Calendar.MONTH 从0开始 + int day = cal.get(Calendar.DAY_OF_MONTH); + + // 星座计算 + if ((month == 3 && day >= 21) || (month == 4 && day <= 19)) { + return "白羊座"; + } else if ((month == 4 && day >= 20) || (month == 5 && day <= 20)) { + return "金牛座"; + } else if ((month == 5 && day >= 21) || (month == 6 && day <= 21)) { + return "双子座"; + } else if ((month == 6 && day >= 22) || (month == 7 && day <= 22)) { + return "巨蟹座"; + } else if ((month == 7 && day >= 23) || (month == 8 && day <= 22)) { + return "狮子座"; + } else if ((month == 8 && day >= 23) || (month == 9 && day <= 22)) { + return "处女座"; + } else if ((month == 9 && day >= 23) || (month == 10 && day <= 23)) { + return "天秤座"; + } else if ((month == 10 && day >= 24) || (month == 11 && day <= 22)) { + return "天蝎座"; + } else if ((month == 11 && day >= 23) || (month == 12 && day <= 21)) { + return "射手座"; + } else if ((month == 12 && day >= 22) || (month == 1 && day <= 19)) { + return "摩羯座"; + } else if ((month == 1 && day >= 20) || (month == 2 && day <= 18)) { + return "水瓶座"; + } else if ((month == 2 && day >= 19) || (month == 3 && day <= 20)) { + return "双鱼座"; + } + return ""; + } catch (ParseException e) { + return ""; } } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java index 68e13fcc..0f750b92 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java @@ -23,8 +23,17 @@ public class SearchActivity extends AppCompatActivity { private final List all = new ArrayList<>(); + private static final String EXTRA_SEARCH_QUERY = "search_query"; + public static void start(Context context) { + start(context, null); + } + + public static void start(Context context, String searchQuery) { Intent intent = new Intent(context, SearchActivity.class); + if (searchQuery != null && !searchQuery.trim().isEmpty()) { + intent.putExtra(EXTRA_SEARCH_QUERY, searchQuery); + } context.startActivity(intent); } @@ -40,6 +49,13 @@ public class SearchActivity extends AppCompatActivity { binding.backButton.setOnClickListener(v -> finish()); binding.cancelBtn.setOnClickListener(v -> finish()); + // 如果从Intent中获取到搜索关键词,自动填充到搜索框 + String searchQuery = getIntent().getStringExtra(EXTRA_SEARCH_QUERY); + if (searchQuery != null && !searchQuery.trim().isEmpty()) { + binding.searchInput.setText(searchQuery); + binding.searchInput.setSelection(searchQuery.length()); + } + binding.searchInput.requestFocus(); } diff --git a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java index 7457f197..283f548b 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java @@ -232,9 +232,8 @@ public class TabPlaceholderActivity extends AppCompatActivity { Toast.makeText(this, "点击:" + user.getName(), Toast.LENGTH_SHORT).show(); }); - StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); - glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); - binding.nearbyRecyclerView.setLayoutManager(glm); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + binding.nearbyRecyclerView.setLayoutManager(layoutManager); binding.nearbyRecyclerView.setAdapter(adapter); adapter.submitList(buildNearbyDemoUsers(18)); @@ -254,10 +253,12 @@ public class TabPlaceholderActivity extends AppCompatActivity { private List buildNearbyDemoUsers(int count) { List list = new ArrayList<>(); + String[] names = {"小王", "小李", "安安", "小陈", "小美", "老张", "小七", "阿杰", + "小雨", "阿宁", "小星", "小林", "小杨", "小刘", "小赵", "小孙", "小周", "小吴"}; for (int i = 0; i < count; i++) { - String id = "nearby-" + i; - String name = "附近主播" + (i + 1); - boolean live = i % 3 == 0; + String id = "user-" + i; + String name = i < names.length ? names[i] : "用户" + (i + 1); + boolean live = false; // 不再显示直播状态 String distanceText; if (i < 3) { distanceText = (300 + i * 120) + "m"; @@ -491,9 +492,7 @@ public class TabPlaceholderActivity extends AppCompatActivity { Toast.makeText(this, "已发送好友请求:" + user.getName(), Toast.LENGTH_SHORT).show(); }); - StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); - glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); - binding.addFriendRecyclerView.setLayoutManager(glm); + binding.addFriendRecyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.addFriendRecyclerView.setAdapter(addFriendAdapter); addFriendAllUsers.clear(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java new file mode 100644 index 00000000..38123d51 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java @@ -0,0 +1,61 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityTableGamesBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class TableGamesActivity extends AppCompatActivity { + + private ActivityTableGamesBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, TableGamesActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityTableGamesBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java b/android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java new file mode 100644 index 00000000..f86d9d52 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java @@ -0,0 +1,74 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.google.android.material.badge.BadgeDrawable; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class UnreadMessageManager { + + private static final String PREFS_NAME = "unread_messages"; + private static final String KEY_UNREAD_COUNT = "unread_count"; + + /** + * 获取总未读消息数量 + */ + public static int getUnreadCount(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getInt(KEY_UNREAD_COUNT, 0); + } + + /** + * 设置总未读消息数量 + */ + public static void setUnreadCount(Context context, int count) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putInt(KEY_UNREAD_COUNT, Math.max(0, count)).apply(); + } + + /** + * 增加未读消息数量 + */ + public static void incrementUnreadCount(Context context, int increment) { + int current = getUnreadCount(context); + setUnreadCount(context, current + increment); + } + + /** + * 减少未读消息数量 + */ + public static void decrementUnreadCount(Context context, int decrement) { + int current = getUnreadCount(context); + setUnreadCount(context, Math.max(0, current - decrement)); + } + + /** + * 更新底部导航栏的消息徽章 + */ + public static void updateBadge(BottomNavigationView bottomNav) { + if (bottomNav == null) return; + + Context context = bottomNav.getContext(); + int unreadCount = getUnreadCount(context); + + BadgeDrawable badge = bottomNav.getOrCreateBadge(R.id.nav_messages); + + if (unreadCount > 0) { + badge.setVisible(true); + badge.setNumber(unreadCount); + // BadgeDrawable 会自动处理超过 99 的情况,显示 "99+" + } else { + badge.setVisible(false); + badge.clearNumber(); + } + } + + /** + * 清除未读消息数量(当用户进入消息页面时) + */ + public static void clearUnreadCount(Context context) { + setUnreadCount(context, 0); + } +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java index 5b8c8553..372cd49f 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java @@ -58,6 +58,13 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { binding.bio.setText(bio != null ? bio : ""); binding.avatar.setImageResource(avatarRes); + // 设置头像点击放大功能 + binding.avatar.setOnClickListener(v -> { + AvatarViewerDialog dialog = AvatarViewerDialog.create(this); + dialog.setAvatarResId(avatarRes); + dialog.show(); + }); + setupTabsAndWorks(); bindDemoStatsAndWorks(userId); @@ -86,8 +93,8 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { if (binding.tabs.getTabCount() == 0) { binding.tabs.addTab(binding.tabs.newTab().setText("作品")); - binding.tabs.addTab(binding.tabs.newTab().setText("动态")); - binding.tabs.addTab(binding.tabs.newTab().setText("资料")); + binding.tabs.addTab(binding.tabs.newTab().setText("收藏")); + binding.tabs.addTab(binding.tabs.newTab().setText("赞过")); } if (worksAdapter == null) { @@ -119,9 +126,10 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { private void showTab(int index) { if (binding == null) return; + // 标签页顺序:0-作品, 1-收藏, 2-赞过 binding.worksRecycler.setVisibility(index == 0 ? android.view.View.VISIBLE : android.view.View.GONE); - binding.dynamicsPlaceholder.setVisibility(index == 1 ? android.view.View.VISIBLE : android.view.View.GONE); - binding.profilePlaceholder.setVisibility(index == 2 ? android.view.View.VISIBLE : android.view.View.GONE); + binding.favoritesPlaceholder.setVisibility(index == 1 ? android.view.View.VISIBLE : android.view.View.GONE); + binding.likedPlaceholder.setVisibility(index == 2 ? android.view.View.VISIBLE : android.view.View.GONE); } private void bindDemoStatsAndWorks(String userId) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java new file mode 100644 index 00000000..fb4ff2f8 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java @@ -0,0 +1,62 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.livestreaming.databinding.ActivityVoiceMatchBinding; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +public class VoiceMatchActivity extends AppCompatActivity { + + private ActivityVoiceMatchBinding binding; + + public static void start(Context context) { + Intent intent = new Intent(context, VoiceMatchActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityVoiceMatchBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setupUI(); + } + + private void setupUI() { + binding.backButton.setOnClickListener(v -> finish()); + + BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation; + bottomNavigation.setSelectedItemId(R.id.nav_friends); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_home) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return true; + } + if (id == R.id.nav_wish_tree) { + WishTreeActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_messages) { + MessagesActivity.start(this); + finish(); + return true; + } + if (id == R.id.nav_profile) { + ProfileActivity.start(this); + finish(); + return true; + } + return true; + }); + } +} + diff --git a/android-app/app/src/main/res/drawable/ic_globe_24.xml b/android-app/app/src/main/res/drawable/ic_globe_24.xml index 8eb416ed..0e696bda 100644 --- a/android-app/app/src/main/res/drawable/ic_globe_24.xml +++ b/android-app/app/src/main/res/drawable/ic_globe_24.xml @@ -6,7 +6,7 @@ android:viewportHeight="24"> diff --git a/android-app/app/src/main/res/layout/activity_draw_guess.xml b/android-app/app/src/main/res/layout/activity_draw_guess.xml new file mode 100644 index 00000000..ae501c1e --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_draw_guess.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_edit_profile.xml b/android-app/app/src/main/res/layout/activity_edit_profile.xml index e0957dee..33a45faf 100644 --- a/android-app/app/src/main/res/layout/activity_edit_profile.xml +++ b/android-app/app/src/main/res/layout/activity_edit_profile.xml @@ -186,7 +186,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:hint="生日(例如 2001-08-08)" + android:hint="生日" app:boxBackgroundMode="filled" app:boxBackgroundColor="@android:color/white" app:boxCornerRadiusTopStart="14dp" @@ -202,7 +202,13 @@ android:id="@+id/inputBirthday" android:layout_width="match_parent" android:layout_height="wrap_content" - android:maxLines="1" /> + android:maxLines="1" + android:focusable="false" + android:focusableInTouchMode="false" + android:clickable="true" + android:cursorVisible="false" + android:enabled="true" + android:inputType="none" /> @@ -211,7 +217,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:hint="性别(男/女/保密)" + android:hint="性别" app:boxBackgroundMode="filled" app:boxBackgroundColor="@android:color/white" app:boxCornerRadiusTopStart="14dp" @@ -227,7 +233,10 @@ android:id="@+id/inputGender" android:layout_width="match_parent" android:layout_height="wrap_content" - android:maxLines="1" /> + android:maxLines="1" + android:focusable="false" + android:clickable="true" + android:cursorVisible="false" /> @@ -236,7 +245,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:hint="所在地(例如 北京·朝阳)" + android:hint="所在地" app:boxBackgroundMode="filled" app:boxBackgroundColor="@android:color/white" app:boxCornerRadiusTopStart="14dp" @@ -252,7 +261,10 @@ android:id="@+id/inputLocation" android:layout_width="match_parent" android:layout_height="wrap_content" - android:maxLines="1" /> + android:maxLines="1" + android:focusable="false" + android:clickable="true" + android:cursorVisible="false" /> diff --git a/android-app/app/src/main/res/layout/activity_find_game.xml b/android-app/app/src/main/res/layout/activity_find_game.xml new file mode 100644 index 00000000..40fbe118 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_find_game.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_fish_pond.xml b/android-app/app/src/main/res/layout/activity_fish_pond.xml index ca931c8e..841d718c 100644 --- a/android-app/app/src/main/res/layout/activity_fish_pond.xml +++ b/android-app/app/src/main/res/layout/activity_fish_pond.xml @@ -4,7 +4,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bg_fishpond_gradient" - android:paddingTop="18dp"> + android:paddingTop="18dp" + android:clipChildren="false" + android:clipToPadding="false"> + @@ -472,7 +476,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/orbitContainer" - app:layout_constraintBottom_toTopOf="@id/bottomAppBar"> + app:layout_constraintBottom_toTopOf="@id/bottomNavInclude"> + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_ktv_together.xml b/android-app/app/src/main/res/layout/activity_ktv_together.xml new file mode 100644 index 00000000..e71a3274 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_ktv_together.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index be266f43..fa8abccb 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -7,7 +7,8 @@ + android:layout_height="match_parent" + android:fitsSystemWindows="false"> @@ -61,17 +64,6 @@ android:text="附近" /> - - + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_peace_elite.xml b/android-app/app/src/main/res/layout/activity_peace_elite.xml new file mode 100644 index 00000000..50d1c44b --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_peace_elite.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_profile.xml b/android-app/app/src/main/res/layout/activity_profile.xml index 9713e7b6..37e47456 100644 --- a/android-app/app/src/main/res/layout/activity_profile.xml +++ b/android-app/app/src/main/res/layout/activity_profile.xml @@ -167,13 +167,6 @@ android:src="@drawable/ic_copy_24" android:tint="#E6FFFFFF" /> - - @@ -264,18 +257,6 @@ - - - - + android:text="赞过" /> diff --git a/android-app/app/src/main/res/layout/activity_table_games.xml b/android-app/app/src/main/res/layout/activity_table_games.xml new file mode 100644 index 00000000..29b06eee --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_table_games.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml b/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml index 85f2c408..7e6bdb72 100644 --- a/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml +++ b/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml @@ -18,20 +18,109 @@ android:paddingTop="14dp" android:paddingBottom="16dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/name" + app:layout_constraintBottom_toBottomOf="@id/name" /> + + + app:layout_constraintStart_toEndOf="@id/space_location_margin_start" + app:layout_constraintTop_toBottomOf="@id/space_location_margin_top" /> + + + app:layout_constraintTop_toBottomOf="@id/space_bio_margin_top" /> + + + app:layout_constraintTop_toBottomOf="@id/space_addFriend_margin_top" /> - @@ -140,10 +288,9 @@ android:id="@+id/statsRow" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="18dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/readOnlyHint"> + app:layout_constraintTop_toBottomOf="@id/space_statsRow_margin_top"> + + + app:layout_constraintTop_toBottomOf="@id/space_tabs_margin_top" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/space_likedPlaceholder_margin_top" /> diff --git a/android-app/app/src/main/res/layout/activity_voice_match.xml b/android-app/app/src/main/res/layout/activity_voice_match.xml new file mode 100644 index 00000000..ed954b4b --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_voice_match.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/bottom_sheet_date_picker.xml b/android-app/app/src/main/res/layout/bottom_sheet_date_picker.xml new file mode 100644 index 00000000..66d13a05 --- /dev/null +++ b/android-app/app/src/main/res/layout/bottom_sheet_date_picker.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/bottom_sheet_location_picker.xml b/android-app/app/src/main/res/layout/bottom_sheet_location_picker.xml new file mode 100644 index 00000000..9e55f346 --- /dev/null +++ b/android-app/app/src/main/res/layout/bottom_sheet_location_picker.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/bottom_sheet_location_picker_spinner.xml b/android-app/app/src/main/res/layout/bottom_sheet_location_picker_spinner.xml new file mode 100644 index 00000000..5ec00079 --- /dev/null +++ b/android-app/app/src/main/res/layout/bottom_sheet_location_picker_spinner.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/dialog_avatar_viewer.xml b/android-app/app/src/main/res/layout/dialog_avatar_viewer.xml new file mode 100644 index 00000000..663baae4 --- /dev/null +++ b/android-app/app/src/main/res/layout/dialog_avatar_viewer.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/android-app/app/src/main/res/layout/include_bottom_nav.xml b/android-app/app/src/main/res/layout/include_bottom_nav.xml index da3ef4e5..38d9501f 100644 --- a/android-app/app/src/main/res/layout/include_bottom_nav.xml +++ b/android-app/app/src/main/res/layout/include_bottom_nav.xml @@ -1,26 +1,20 @@ - - - - - + android:layout_height="48dp" + android:minHeight="48dp" + android:background="@android:color/white" + android:paddingTop="2dp" + android:paddingBottom="2dp" + android:fitsSystemWindows="false" + android:elevation="8dp" + app:itemIconTint="@color/bottom_nav_item_color" + app:itemTextColor="@color/bottom_nav_item_color" + app:itemIconSize="22dp" + app:itemTextAppearanceActive="@style/BottomNavigationView.TextAppearance" + app:itemTextAppearanceInactive="@style/BottomNavigationView.TextAppearance" + app:labelVisibilityMode="labeled" + app:menu="@menu/bottom_nav_main" /> diff --git a/android-app/app/src/main/res/layout/item_conversation.xml b/android-app/app/src/main/res/layout/item_conversation.xml index 623c30a0..7de6a357 100644 --- a/android-app/app/src/main/res/layout/item_conversation.xml +++ b/android-app/app/src/main/res/layout/item_conversation.xml @@ -11,8 +11,9 @@ android:id="@+id/avatar" android:layout_width="44dp" android:layout_height="44dp" - android:background="@drawable/bg_avatar_circle" - android:padding="6dp" + android:background="@drawable/bg_avatar_circle_transparent" + android:clipToOutline="true" + android:scaleType="centerCrop" android:src="@drawable/ic_account_circle_24" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/android-app/app/src/main/res/layout/item_conversation_message_incoming.xml b/android-app/app/src/main/res/layout/item_conversation_message_incoming.xml index 56153e6a..6e97d6f0 100644 --- a/android-app/app/src/main/res/layout/item_conversation_message_incoming.xml +++ b/android-app/app/src/main/res/layout/item_conversation_message_incoming.xml @@ -19,8 +19,9 @@ android:id="@+id/avatarView" android:layout_width="34dp" android:layout_height="34dp" - android:background="@drawable/bg_avatar_circle" - android:padding="5dp" + android:background="@drawable/bg_avatar_circle_transparent" + android:clipToOutline="true" + android:scaleType="centerCrop" android:src="@drawable/ic_account_circle_24" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml b/android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml index 5e8a35cc..21031661 100644 --- a/android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml +++ b/android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml @@ -19,8 +19,9 @@ android:id="@+id/avatarView" android:layout_width="34dp" android:layout_height="34dp" - android:background="@drawable/bg_avatar_circle" - android:padding="5dp" + android:background="@drawable/bg_avatar_circle_transparent" + android:clipToOutline="true" + android:scaleType="centerCrop" android:src="@drawable/ic_account_circle_24" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/android-app/app/src/main/res/layout/item_nearby_user.xml b/android-app/app/src/main/res/layout/item_nearby_user.xml index 1723e6a5..c6d0bd9a 100644 --- a/android-app/app/src/main/res/layout/item_nearby_user.xml +++ b/android-app/app/src/main/res/layout/item_nearby_user.xml @@ -1,78 +1,78 @@ - + android:layout_height="72dp" + android:background="@android:color/white" + android:paddingStart="16dp" + android:paddingEnd="16dp"> - + + + android:layout_marginStart="12dp" + android:layout_marginEnd="12dp" + android:ellipsize="end" + android:maxLines="1" + android:text="用户昵称" + android:textColor="#111111" + android:textSize="15sp" + android:textStyle="bold" + app:layout_constraintEnd_toStartOf="@id/addButton" + app:layout_constraintStart_toEndOf="@id/avatarImage" + app:layout_constraintTop_toTopOf="@id/avatarImage" /> - + - + - + - - - - - + diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml index 1204dd61..18c0168f 100644 --- a/android-app/app/src/main/res/values/themes.xml +++ b/android-app/app/src/main/res/values/themes.xml @@ -34,4 +34,9 @@ 50% + + +