diff --git a/android/app/build.gradle b/android/app/build.gradle index 394f14c..560d28f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -58,6 +58,7 @@ dependencies { implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" + implementation "androidx.viewpager2:viewpager2:1.1.0" testImplementation "junit:junit:$junitVersion" testImplementation "org.robolectric:robolectric:4.14.1" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" 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 fcc756f..ba3e08d 100644 --- a/android/app/src/main/java/com/hyzq/boss/MainActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MainActivity.java @@ -6,9 +6,10 @@ import android.os.Handler; import android.os.Looper; import android.text.Editable; import android.text.TextWatcher; -import android.view.GestureDetector; -import android.view.MotionEvent; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; @@ -19,7 +20,9 @@ import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager2.widget.ViewPager2; import org.json.JSONArray; import org.json.JSONObject; @@ -60,6 +63,7 @@ public class MainActivity extends AppCompatActivity { private Button tabConversations; private Button tabDevices; private Button tabMe; + private ViewPager2 rootPager; private SwipeRefreshLayout screenRefresh; private ScrollView screenScroll; private LinearLayout screenContent; @@ -81,7 +85,8 @@ public class MainActivity extends AppCompatActivity { private boolean conversationAutoRefreshArmed = false; private boolean conversationAutoRefreshEnabled = false; private final Set selectedConversationProjectIds = new LinkedHashSet<>(); - private @Nullable GestureDetector conversationPagerGestureDetector; + private @Nullable RootPagerAdapter rootPagerAdapter; + private boolean syncingRootPagerSelection = false; private final Runnable conversationAutoRefreshRunnable = new Runnable() { @Override public void run() { @@ -175,9 +180,7 @@ public class MainActivity extends AppCompatActivity { tabConversations = findViewById(R.id.tab_conversations); tabDevices = findViewById(R.id.tab_devices); tabMe = findViewById(R.id.tab_me); - screenRefresh = findViewById(R.id.screen_refresh); - screenScroll = findViewById(R.id.screen_scroll); - screenContent = findViewById(R.id.screen_content); + rootPager = findViewById(R.id.root_pager); String[] rootTabs = WechatSurfaceMapper.rootTabLabels(); tabConversations.setText(rootTabs[0]); @@ -197,47 +200,23 @@ public class MainActivity extends AppCompatActivity { tabConversations.setOnClickListener(v -> setActiveTab("conversations", true)); tabDevices.setOnClickListener(v -> setActiveTab("devices", true)); tabMe.setOnClickListener(v -> setActiveTab("me", true)); - screenRefresh.setOnRefreshListener(this::refreshCurrentTab); - conversationPagerGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { - private static final int SWIPE_DISTANCE_THRESHOLD = 120; - private static final int SWIPE_VELOCITY_THRESHOLD = 120; - + rootPagerAdapter = new RootPagerAdapter(); + rootPager.setAdapter(rootPagerAdapter); + rootPager.setOffscreenPageLimit(3); + rootPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override - public boolean onDown(MotionEvent e) { - return true; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (e1 == null || e2 == null) { - return false; + public void onPageSelected(int position) { + if (syncingRootPagerSelection) { + return; } - float deltaX = e2.getX() - e1.getX(); - float deltaY = e2.getY() - e1.getY(); - if (Math.abs(deltaX) < Math.abs(deltaY)) { - return false; - } - if (Math.abs(deltaX) < SWIPE_DISTANCE_THRESHOLD || Math.abs(velocityX) < SWIPE_VELOCITY_THRESHOLD) { - return false; - } - handleHorizontalPageSwipe(deltaX < 0 ? 1 : -1); - return true; + handleRootPagerSelection(position); } }); - screenRefresh.setOnTouchListener((v, event) -> { - if (conversationPagerGestureDetector != null) { - conversationPagerGestureDetector.onTouchEvent(event); - } - return false; + rootPager.post(() -> { + syncActivePageViews(activeTab); + renderCurrentTab(); + updateConversationAutoRefresh(); }); - if (screenScroll != null) { - screenScroll.setOnTouchListener((v, event) -> { - if (conversationPagerGestureDetector != null) { - conversationPagerGestureDetector.onTouchEvent(event); - } - return false; - }); - } } private void applyInitialTab(@Nullable Intent intent) { @@ -462,6 +441,8 @@ public class MainActivity extends AppCompatActivity { } lastRootBackPressedAt = 0L; updateTabStyles(); + syncRootPager(tab, fromUser); + syncActivePageViews(tab); renderCurrentTab(); updateConversationAutoRefresh(); } @@ -552,6 +533,9 @@ public class MainActivity extends AppCompatActivity { } private void renderConversationsRoot() { + if (screenContent == null) { + return; + } screenContent.removeAllViews(); screenContent.addView(buildConversationSearchInput()); if (conversationSelectionMode) { @@ -756,20 +740,14 @@ public class MainActivity extends AppCompatActivity { }); } - private void handleHorizontalPageSwipe(int direction) { - String[] order = new String[] {"conversations", "devices", "me"}; - int currentIndex = 0; - for (int i = 0; i < order.length; i++) { - if (order[i].equals(activeTab)) { - currentIndex = i; - break; - } - } - int nextIndex = currentIndex + direction; - if (nextIndex < 0 || nextIndex >= order.length) { + private void handleRootPagerSelection(int position) { + String tab = tabForIndex(position); + if (tab.equals(activeTab)) { + syncActivePageViews(tab); + updateConversationAutoRefresh(); return; } - setActiveTab(order[nextIndex], true); + setActiveTab(tab, true); } private EditText buildConversationSearchInput() { @@ -848,6 +826,9 @@ public class MainActivity extends AppCompatActivity { } private void renderDevicesRoot() { + if (screenContent == null) { + return; + } screenContent.removeAllViews(); if (devicesData == null || devicesData.length() == 0) { screenContent.addView(BossUi.buildEmptyCard(this, "当前没有接入设备。")); @@ -875,6 +856,9 @@ public class MainActivity extends AppCompatActivity { } private void renderMeRoot() { + if (screenContent == null) { + return; + } screenContent.removeAllViews(); String displayName = sessionData == null ? apiClient.getDisplayName() @@ -945,6 +929,9 @@ public class MainActivity extends AppCompatActivity { } private void startRefreshing(boolean refreshing) { + if (screenRefresh == null) { + return; + } screenRefresh.setRefreshing(refreshing); syncTopActionVisualState(refreshing); if (!refreshing) { @@ -1113,4 +1100,144 @@ public class MainActivity extends AppCompatActivity { return getSharedPreferences(UI_PREFS, MODE_PRIVATE) .getString(KEY_LAST_ROOT_TAB, null); } + + private void syncRootPager(String tab, boolean smoothScroll) { + if (rootPager == null) { + return; + } + int targetIndex = indexForTab(tab); + if (targetIndex < 0 || rootPager.getCurrentItem() == targetIndex) { + return; + } + syncingRootPagerSelection = true; + rootPager.setCurrentItem(targetIndex, smoothScroll); + rootPager.post(() -> syncingRootPagerSelection = false); + } + + private void syncActivePageViews(String tab) { + if (rootPagerAdapter == null) { + screenRefresh = null; + screenScroll = null; + screenContent = null; + return; + } + RootPageViewHolder holder = rootPagerAdapter.findHolder(tab); + if (holder == null) { + screenRefresh = null; + screenScroll = null; + screenContent = null; + rootPager.post(() -> { + RootPageViewHolder pendingHolder = rootPagerAdapter.findHolder(activeTab); + if (pendingHolder != null) { + screenRefresh = pendingHolder.refresh; + screenScroll = pendingHolder.scroll; + screenContent = pendingHolder.content; + renderCurrentTab(); + updateConversationAutoRefresh(); + } + }); + return; + } + screenRefresh = holder.refresh; + screenScroll = holder.scroll; + screenContent = holder.content; + } + + private static int indexForTab(String tab) { + switch (tab) { + case "devices": + return 1; + case "me": + return 2; + case "conversations": + default: + return 0; + } + } + + private static String tabForIndex(int index) { + switch (index) { + case 1: + return "devices"; + case 2: + return "me"; + case 0: + default: + return "conversations"; + } + } + + private final class RootPagerAdapter extends RecyclerView.Adapter { + private final String[] tabs = new String[] {"conversations", "devices", "me"}; + private final RootPageViewHolder[] holders = new RootPageViewHolder[tabs.length]; + + RootPagerAdapter() { + LayoutInflater inflater = LayoutInflater.from(MainActivity.this); + for (int i = 0; i < tabs.length; i++) { + RootPageViewHolder holder = RootPageViewHolder.create(inflater, rootPager, tabs[i]); + holder.refresh.setOnRefreshListener(() -> { + if (!holder.tab.equals(activeTab)) { + setActiveTab(holder.tab, true); + return; + } + refreshCurrentTab(); + }); + holders[i] = holder; + } + } + + @Override + public int getItemCount() { + return tabs.length; + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public RootPageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + RootPageViewHolder holder = holders[viewType]; + ViewParent currentParent = holder.itemView.getParent(); + if (currentParent instanceof ViewGroup) { + ((ViewGroup) currentParent).removeView(holder.itemView); + } + return holder; + } + + @Override + public void onBindViewHolder(RootPageViewHolder holder, int position) { + holders[position] = holder; + } + + @Nullable + RootPageViewHolder findHolder(String tab) { + int index = indexForTab(tab); + if (index < 0 || index >= holders.length) { + return null; + } + return holders[index]; + } + } + + private static final class RootPageViewHolder extends RecyclerView.ViewHolder { + final String tab; + final SwipeRefreshLayout refresh; + final ScrollView scroll; + final LinearLayout content; + + static RootPageViewHolder create(LayoutInflater inflater, ViewGroup parent, String tab) { + View view = inflater.inflate(R.layout.view_root_tab_page, parent, false); + return new RootPageViewHolder(view, tab); + } + + RootPageViewHolder(View itemView, String tab) { + super(itemView); + this.tab = tab; + this.refresh = itemView.findViewById(R.id.root_page_refresh); + this.scroll = itemView.findViewById(R.id.root_page_scroll); + this.content = itemView.findViewById(R.id.root_page_content); + } + } } diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 5d4e1b5..d341349 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -91,7 +91,7 @@ android:orientation="horizontal" android:paddingLeft="20dp" android:paddingTop="14dp" - android:paddingRight="32dp" + android:paddingRight="20dp" android:paddingBottom="12dp">