From 9d19163b0df28bbef166042e91e8dd0464310db2 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 4 Apr 2026 02:19:27 +0800 Subject: [PATCH] feat: add conversation quick actions fan menu --- .../main/java/com/hyzq/boss/MainActivity.java | 178 +++++++++++++++++- .../com/hyzq/boss/WechatSurfaceMapper.java | 2 +- .../app/src/main/res/layout/activity_main.xml | 78 ++++++++ .../hyzq/boss/GroupCreateActivityTest.java | 89 ++++++++- ...MainActivityConversationSelectionTest.java | 58 +++++- .../WechatSurfaceMapperTopActionTest.java | 2 +- 6 files changed, 388 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/com/hyzq/boss/MainActivity.java b/android/app/src/main/java/com/hyzq/boss/MainActivity.java index d07c9ed..9d467a1 100644 --- a/android/app/src/main/java/com/hyzq/boss/MainActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MainActivity.java @@ -10,6 +10,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; @@ -65,6 +66,11 @@ public class MainActivity extends AppCompatActivity { private EditText topSearchInput; private Button searchButton; private Button refreshButton; + private View conversationQuickActionsOverlay; + private View conversationQuickActionsScrim; + private View quickActionAddDevice; + private View quickActionScan; + private View quickActionGroupChat; private Button tabConversations; private Button tabDevices; private Button tabMe; @@ -89,6 +95,7 @@ public class MainActivity extends AppCompatActivity { private boolean pinnedConversationsCollapsed = false; private boolean conversationSelectionMode = false; private boolean conversationSearchMode = false; + private boolean conversationQuickActionsVisible = false; private boolean conversationAutoRefreshArmed = false; private boolean conversationAutoRefreshEnabled = false; private final Set selectedConversationProjectIds = new LinkedHashSet<>(); @@ -136,6 +143,10 @@ public class MainActivity extends AppCompatActivity { exitConversationSearchMode(true); return; } + if (contentPanel.getVisibility() == View.VISIBLE && conversationQuickActionsVisible) { + hideConversationQuickActions(true); + return; + } if (contentPanel.getVisibility() == View.VISIBLE && !"conversations".equals(activeTab)) { setActiveTab("conversations", false); persistLastRootTab("conversations"); @@ -191,6 +202,11 @@ public class MainActivity extends AppCompatActivity { topSearchInput = findViewById(R.id.top_search_input); searchButton = findViewById(R.id.search_button); refreshButton = findViewById(R.id.refresh_button); + conversationQuickActionsOverlay = findViewById(R.id.conversation_quick_actions_overlay); + conversationQuickActionsScrim = findViewById(R.id.conversation_quick_actions_scrim); + quickActionAddDevice = findViewById(R.id.quick_action_add_device); + quickActionScan = findViewById(R.id.quick_action_scan); + quickActionGroupChat = findViewById(R.id.quick_action_group_chat); tabConversations = findViewById(R.id.tab_conversations); tabDevices = findViewById(R.id.tab_devices); tabMe = findViewById(R.id.tab_me); @@ -221,6 +237,19 @@ public class MainActivity extends AppCompatActivity { } }); refreshButton.setOnClickListener(v -> handleTopAction()); + conversationQuickActionsScrim.setOnClickListener(v -> hideConversationQuickActions(true)); + quickActionAddDevice.setOnClickListener(v -> { + hideConversationQuickActions(false); + startActivity(new Intent(this, DeviceEnrollmentActivity.class)); + }); + quickActionScan.setOnClickListener(v -> { + hideConversationQuickActions(false); + showMessage("扫一扫即将接入"); + }); + quickActionGroupChat.setOnClickListener(v -> { + hideConversationQuickActions(false); + enterConversationSelectionMode(); + }); tabConversations.setOnClickListener(v -> setActiveTab("conversations", true)); tabDevices.setOnClickListener(v -> setActiveTab("devices", true)); tabMe.setOnClickListener(v -> setActiveTab("me", true)); @@ -478,6 +507,7 @@ public class MainActivity extends AppCompatActivity { if (!"conversations".equals(tab)) { exitConversationSelectionMode(); exitConversationSearchMode(false); + hideConversationQuickActions(false); } activeTab = tab; if (fromUser) { @@ -612,7 +642,7 @@ public class MainActivity extends AppCompatActivity { exitConversationSelectionMode(); return; } - enterConversationSelectionMode(); + toggleConversationQuickActions(); return; } String actionKey = WechatSurfaceMapper.rootTopAction(activeTab, false, conversationSelectionMode).actionKey; @@ -640,8 +670,12 @@ public class MainActivity extends AppCompatActivity { appendConversationSelectionControls(items); } JSONArray filteredConversations = filterConversationItems(conversationsData, conversationSearchQuery); + if (conversationSelectionMode) { + filteredConversations = filterManualGroupSelectableConversationItems(filteredConversations); + } if (filteredConversations == null || filteredConversations.length() == 0) { - items.add(() -> BossUi.buildEmptyCard(this, "当前没有会话数据。")); + String emptyText = conversationSelectionMode ? "当前没有可发起群聊的线程。" : "当前没有会话数据。"; + items.add(() -> BossUi.buildEmptyCard(this, emptyText)); showListPage(items); return; } @@ -722,15 +756,15 @@ public class MainActivity extends AppCompatActivity { private void appendConversationSelectionControls(List items) { items.add(() -> buildSelectionSummaryView()); + items.add(() -> { + Button cancelButton = BossUi.buildMiniActionButton(this, "取消", false); + cancelButton.setOnClickListener(v -> exitConversationSelectionMode()); - Button cancelButton = BossUi.buildMiniActionButton(this, "取消", false); - cancelButton.setOnClickListener(v -> exitConversationSelectionMode()); - - Button createButton = BossUi.buildMiniActionButton(this, "发起群聊", true); - createButton.setEnabled(selectedConversationProjectIds.size() >= 2); - createButton.setOnClickListener(v -> createStandaloneGroupChatFromSelection()); - - items.add(() -> BossUi.buildInlineActionRow(this, cancelButton, createButton)); + Button createButton = BossUi.buildMiniActionButton(this, "发起群聊", true); + createButton.setEnabled(selectedConversationProjectIds.size() >= 2); + createButton.setOnClickListener(v -> createStandaloneGroupChatFromSelection()); + return BossUi.buildInlineActionRow(this, cancelButton, createButton); + }); } private LinearLayout buildSelectionSummaryView() { @@ -768,6 +802,7 @@ public class MainActivity extends AppCompatActivity { private void enterConversationSelectionMode() { exitConversationSearchMode(false); + hideConversationQuickActions(false); conversationSelectionMode = true; selectedConversationProjectIds.clear(); syncTopActionVisualState(screenRefresh.isRefreshing()); @@ -790,6 +825,7 @@ public class MainActivity extends AppCompatActivity { if (!"conversations".equals(activeTab)) { return; } + hideConversationQuickActions(false); conversationSearchMode = true; syncTopActionVisualState(screenRefresh.isRefreshing()); topSearchInput.post(() -> { @@ -900,6 +936,128 @@ public class MainActivity extends AppCompatActivity { return filtered; } + static JSONArray filterManualGroupSelectableConversationItems(@Nullable JSONArray source) { + if (source == null) { + return null; + } + JSONArray filtered = new JSONArray(); + for (int i = 0; i < source.length(); i++) { + JSONObject item = source.optJSONObject(i); + if (GroupCreateActivity.isEligibleForManualGroupSelection(item, null)) { + filtered.put(item); + } + } + return filtered; + } + + private void toggleConversationQuickActions() { + if (conversationQuickActionsVisible) { + hideConversationQuickActions(true); + return; + } + showConversationQuickActions(); + } + + private void showConversationQuickActions() { + if (conversationQuickActionsOverlay == null || conversationQuickActionsVisible + || !"conversations".equals(activeTab) || conversationSelectionMode || conversationSearchMode) { + return; + } + conversationQuickActionsVisible = true; + conversationQuickActionsOverlay.setVisibility(View.VISIBLE); + conversationQuickActionsOverlay.bringToFront(); + conversationQuickActionsScrim.setAlpha(0f); + prepareConversationQuickAction(quickActionAddDevice); + prepareConversationQuickAction(quickActionScan); + prepareConversationQuickAction(quickActionGroupChat); + conversationQuickActionsScrim.animate() + .alpha(1f) + .setDuration(160L) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + animateConversationQuickAction(quickActionAddDevice, -BossUi.dp(this, 12), BossUi.dp(this, 84), 0L); + animateConversationQuickAction(quickActionScan, -BossUi.dp(this, 90), BossUi.dp(this, 50), 24L); + animateConversationQuickAction(quickActionGroupChat, -BossUi.dp(this, 172), BossUi.dp(this, 14), 48L); + } + + private void hideConversationQuickActions(boolean animated) { + if (conversationQuickActionsOverlay == null) { + return; + } + if (!conversationQuickActionsVisible && conversationQuickActionsOverlay.getVisibility() != View.VISIBLE) { + return; + } + conversationQuickActionsVisible = false; + if (!animated) { + conversationQuickActionsOverlay.setVisibility(View.GONE); + conversationQuickActionsScrim.setAlpha(0f); + resetConversationQuickAction(quickActionAddDevice); + resetConversationQuickAction(quickActionScan); + resetConversationQuickAction(quickActionGroupChat); + return; + } + conversationQuickActionsScrim.animate() + .alpha(0f) + .setDuration(140L) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + collapseConversationQuickAction(quickActionAddDevice, 0L); + collapseConversationQuickAction(quickActionScan, 20L); + collapseConversationQuickAction(quickActionGroupChat, 40L); + conversationQuickActionsOverlay.postDelayed(() -> { + if (!conversationQuickActionsVisible) { + conversationQuickActionsOverlay.setVisibility(View.GONE); + resetConversationQuickAction(quickActionAddDevice); + resetConversationQuickAction(quickActionScan); + resetConversationQuickAction(quickActionGroupChat); + } + }, 190L); + } + + private void prepareConversationQuickAction(View actionView) { + actionView.setVisibility(View.VISIBLE); + actionView.setAlpha(0f); + actionView.setScaleX(0.86f); + actionView.setScaleY(0.86f); + actionView.setTranslationX(0f); + actionView.setTranslationY(0f); + } + + private void animateConversationQuickAction(View actionView, float translationX, float translationY, long delayMs) { + actionView.animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .translationX(translationX) + .translationY(translationY) + .setStartDelay(delayMs) + .setDuration(180L) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + } + + private void collapseConversationQuickAction(View actionView, long delayMs) { + actionView.animate() + .alpha(0f) + .scaleX(0.88f) + .scaleY(0.88f) + .translationX(0f) + .translationY(0f) + .setStartDelay(delayMs) + .setDuration(140L) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + } + + private void resetConversationQuickAction(View actionView) { + actionView.animate().cancel(); + actionView.setAlpha(0f); + actionView.setScaleX(0.86f); + actionView.setScaleY(0.86f); + actionView.setTranslationX(0f); + actionView.setTranslationY(0f); + } + static boolean matchesConversationQuery(JSONObject item, String rawQuery) { if (item == null) { return false; diff --git a/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java b/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java index 72f3d94..fa45483 100644 --- a/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java +++ b/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java @@ -235,7 +235,7 @@ public final class WechatSurfaceMapper { if (selectionMode) { return new RootTopAction("取消", false, false, "cancel_select_conversations"); } - return new RootTopAction("+", false, true, "select_conversations"); + return new RootTopAction("+", false, true, "open_conversation_quick_actions"); } return new RootTopAction(refreshing ? "同步中" : "刷新", false, false, "refresh"); } diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index d6e732c..e1ba745 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -251,4 +251,82 @@ + + + + + + +