From 3df944d30530be8dc0ea1cbe1ed4afc22eb160a5 Mon Sep 17 00:00:00 2001
From: zuoxiao <470321431@qq.com>
Date: 星期四, 06 二月 2025 11:09:48 +0800
Subject: [PATCH] 1.添加数据更新功能,确保本地没有数据时再获取基础数据 2.地图界面上添加滚动功能的控件(部分功能)

---
 app/src/main/java/com/dayu/pipirrapp/fragment/MyFragment.java         |    5 
 app/src/main/java/com/dayu/pipirrapp/net/Constants.java               |    4 
 app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java         |    4 
 app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java               |    4 
 app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java               |    5 
 expand_button/src/main/res/drawable/ic_triangle.xml                   |   14 +
 app/src/main/res/layout/fragment_map.xml                              |   19 +
 app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java          |   11 
 expand_button/src/main/res/values/attrs.xml                           |   15 +
 app/src/main/res/layout/fragment_my.xml                               |   27 ++
 expand_button/src/main/java/com/example/expand_button/ExpandButton.kt |  307 ++++++++++++++++++++++++++++++
 app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java        |  165 ++++++++++++---
 app/build.gradle                                                      |    3 
 13 files changed, 536 insertions(+), 47 deletions(-)

diff --git a/app/build.gradle b/app/build.gradle
index 2211ad5..c28c784 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -89,7 +89,8 @@
 }
 
 dependencies {
-    implementation project(':library')
+    implementation project(':expand_button')
+    implementation(project(':library'))
     implementation project(':date_time_picker')
     implementation 'androidx.appcompat:appcompat:1.6.1'
     implementation 'com.google.android.material:material:1.8.0'
diff --git a/app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java b/app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java
index 8672cc1..05cb87e 100644
--- a/app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java
+++ b/app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java
@@ -8,7 +8,9 @@
 import androidx.room.Update;
 
 import com.dayu.pipirrapp.bean.db.CenterPointBean;
-import com.dayu.pipirrapp.bean.db.TagBean;
+
+import io.reactivex.rxjava3.core.Maybe;
+
 
 /**
  * author: zuo
@@ -18,8 +20,11 @@
  */
 @Dao
 public interface CenterPointDao {
+    @Query("SELECT * FROM CenterPointBean LIMIT 1")
+    Maybe<CenterPointBean> findFirst();
+
     @Insert(onConflict = OnConflictStrategy.REPLACE)
-    void insert(CenterPointBean adminData);
+    void insert(CenterPointBean centerPointBean);
 
     @Update
     void update(CenterPointBean adminData);
@@ -27,6 +32,4 @@
     @Delete
     void delete(CenterPointBean adminData);
 
-    @Query("select  * from CenterPointBean limit 1")
-    CenterPointBean findFirst();
 }
diff --git a/app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java b/app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java
index aad77d8..aa9295a 100644
--- a/app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java
+++ b/app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java
@@ -14,6 +14,7 @@
 
 import io.reactivex.rxjava3.core.Completable;
 import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.core.Maybe;
 
 /**
  * DivideDao -
@@ -50,4 +51,7 @@
 
     @Query("select  * from DivideBean")
     Single<List<DivideBean>> findAllToSingle();
+
+    @Query("SELECT * FROM divide")
+    Maybe<List<DivideBean>> getAll();  // 鏀逛负杩斿洖Maybe<List<DivideBean>>
 }
diff --git a/app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java b/app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java
index 295e868..0323b6d 100644
--- a/app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java
+++ b/app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java
@@ -13,6 +13,7 @@
 
 import io.reactivex.rxjava3.core.Completable;
 import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.core.Maybe;
 
 /**
  * author: zuo
@@ -48,4 +49,8 @@
 
     @Query("select  * from MarkerBean")
     Single<List<MarkerBean>> findAllToSingle();
+
+    @Query("SELECT * FROM MarkerBean")
+    Maybe<List<MarkerBean>> getAll();
+
 }
diff --git a/app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java b/app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java
index 39ec637..ddfb4c4 100644
--- a/app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java
+++ b/app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java
@@ -78,6 +78,7 @@
 import java.util.stream.Collectors;
 
 import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
 import io.reactivex.rxjava3.schedulers.Schedulers;
 
 /**
@@ -116,6 +117,9 @@
 
     MarkerBean mMarkerBean;
 
+    // 娣诲姞CompositeDisposable鏉ョ鐞嗘墍鏈夎闃�
+    private CompositeDisposable compositeDisposable = new CompositeDisposable();
+
     @Override
     public void onAttach(@NonNull Context context) {
         super.onAttach(context);
@@ -130,6 +134,13 @@
         setRetainInstance(true);
         Log.i(TAG, "onCreate");
         mInspectionState = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.inspectionState, 0);
+
+        // 娣诲姞鍒锋柊鏁版嵁鐨勭洃鍚�
+        LiveEventBus.get(CommonKeyName.refreshData).observe(this, o -> {
+            getCenterPoint();
+            getMarkerData();
+            getDivideList();
+        });
     }
 
     @Override
@@ -142,7 +153,6 @@
     @Nullable
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
-
         binding = FragmentMapBinding.inflate(inflater, container, false);
         mapFragmenObserver.setmWebView(binding.webView);
         Log.i("MapFragment", "onCreateView");
@@ -151,15 +161,87 @@
         MyWebViewInterface myWebViewInterface = new MyWebViewInterface(MapFragment.this);
         mWebView.addJavascriptInterface(myWebViewInterface, "Android");
         mWebView.loadUrl("file:///android_asset/index.html");
-        getCenterPoint();
+
+        // 寮傛鍔犺浇鏈湴鏁版嵁
+        loadLocalData();
+
         initView();
         initWeb();
 
-        getMarkerData();
-        getDivideList();
-
         chageInspecState(mInspectionState);
         return binding.getRoot();
+    }
+
+    /**
+     * 寮傛鍔犺浇鏈湴鏁版嵁
+     */
+    private void loadLocalData() {
+        // 寮傛鍔犺浇涓績鐐规暟鎹�
+        compositeDisposable.add(
+            DaoSingleton.getAsynchInstance(this.getContext()).centerPointDao().findFirst()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(result -> {
+                    centerPointBean = result;
+                    if (centerPointBean == null) {
+                        getCenterPoint();
+                    } else {
+                        jumpCenterPoint();
+                    }
+                }, throwable -> {
+                    Log.e(TAG, "Load centerPoint error: " + throwable);
+                    getCenterPoint();
+                }, () -> {
+                    // 褰揗aybe涓虹┖鏃惰皟鐢�
+                    getCenterPoint();
+                })
+        );
+
+        // 寮傛鍔犺浇鍙栨按鍙f暟鎹�
+        compositeDisposable.add(
+            DaoSingleton.getAsynchInstance(this.getContext()).markerDao().getAll()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(markers -> {
+                    if (markers == null || markers.isEmpty()) {
+                        getMarkerData();
+                    } else {
+                        for (MarkerBean marker : markers) {
+                            markerBeanSet.put(marker.getId(), marker);
+                            setMapMarker(marker);
+                        }
+                    }
+                }, throwable -> {
+                    Log.e(TAG, "Load markers error: " + throwable.getMessage());
+                    getMarkerData();
+                }, () -> {
+                    // 褰揗aybe涓虹┖鏃惰皟鐢�
+                    getMarkerData();
+                })
+        );
+
+        // 寮傛鍔犺浇鍒嗘按鎴挎暟鎹�
+        compositeDisposable.add(
+            DaoSingleton.getAsynchInstance(this.getContext()).divideDao().getAll()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(divides -> {
+                    if (divides == null || divides.isEmpty()) {
+                        getDivideList();
+                    } else {
+                        for (DivideBean divide : divides) {
+                            divideBeanMap.put(divide.getId(), divide);
+                            setMapDivide(divide);
+                        }
+                    }
+                }, throwable -> {
+                    Log.e(TAG, "Load divides error: " + throwable.getMessage());
+                    getDivideList();
+                }, () -> {
+                    // 褰揗aybe涓虹┖鏃惰皟鐢�
+                    getDivideList();
+                })
+        );
     }
 
     private void initWeb() {
@@ -232,9 +314,10 @@
      * web鍔犺浇瀹屽垵濮嬪寲鏈湴鏁版嵁
      */
     public void webFinishInitLocalData() {
-        //璺宠浆涓績鐐�
-        centerPointBean = DaoSingleton.getInstance(MapFragment.this.getContext()).centerPointDao().findFirst();
-        jumpCenterPoint();
+        // 鍙湪鏈湴娌℃湁鏁版嵁鏃惰幏鍙栦腑蹇冪偣
+        if (centerPointBean != null) {
+            jumpCenterPoint();
+        }
         //娣诲姞鍥爓ebview娌℃湁鍔犺浇瀹屾垚瀵艰嚧娌℃湁娣诲姞鐨勫湴鍥炬爣娉�
         if (!webNoFinishMarkerData.isEmpty()) {
             for (MarkerBean bean : webNoFinishMarkerData) {
@@ -285,18 +368,22 @@
                                 })
                                 .collect(Collectors.toList());
 
-                        DaoSingleton.getInstance(MapFragment.this.getContext()).markerDao().deleteAll();
-                        // 浣跨敤 RxJava 寮傛鎻掑叆鏁版嵁
-                        DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).markerDao().insertAll(markerBeans)
-                                .subscribeOn(Schedulers.io()) // 鍦� IO 绾跨▼涓婃墽琛�
-                                .observeOn(AndroidSchedulers.mainThread()) // 鍦ㄤ富绾跨▼涓婅瀵�
-                                .subscribe(() -> {
-                                    // 鎻掑叆鎴愬姛
-                                    Log.i("mWebView", "鏁版嵁鎻掑叆鎴愬姛");
-                                }, throwable -> {
-                                    // 鎻掑叆澶辫触
-                                    Log.e("mWebView", "鏁版嵁鎻掑叆澶辫触: " + throwable.getMessage());
-                                });
+                        // 浣跨敤 CompositeDisposable 绠$悊鏁版嵁搴撴彃鍏ユ搷浣�
+                        compositeDisposable.add(
+                            DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).markerDao().insertAll(markerBeans)
+                                .subscribeOn(Schedulers.io())
+                                .observeOn(AndroidSchedulers.mainThread())
+                                .subscribe(
+                                    () -> {
+                                        // 鎻掑叆鎴愬姛
+                                        Log.i("mWebView", "鏁版嵁鎻掑叆鎴愬姛");
+                                    },
+                                    throwable -> {
+                                        // 鎻掑叆澶辫触
+                                        Log.e("mWebView", "鏁版嵁鎻掑叆澶辫触: " + throwable.getMessage());
+                                    }
+                                )
+                        );
                     }
 
                 } else {
@@ -629,7 +716,7 @@
     }
 
     /**
-     * 娣诲姞鍙栨按鍙f爣娉�
+     * 娣诲姞绠$綉鏍囨敞
      */
     public void setMapDivide(PipeNetworkBean pipeNetworkBean) {
 //        if (divide != null) {
@@ -917,27 +1004,30 @@
                 try {
                     if (t.isSuccess()) {
                         if (t.getContent().getObj() != null && !t.getContent().getObj().isEmpty()) {
-
                             List<DivideBean> divideBeans = new ArrayList<>();
                             for (DivideResult divideResult : t.getContent().getObj()) {
                                 DivideBean divideBean = getDivideBean(divideResult);
                                 setMapDivide(divideBean);
                                 divideBeans.add(divideBean);
                             }
-                            // 浣跨敤 RxJava 寮傛鎻掑叆鏁版嵁
-                            DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).divideDao().insertAll(divideBeans)
-                                    .subscribeOn(Schedulers.io()) // 鍦� IO 绾跨▼涓婃墽琛�
-                                    .observeOn(AndroidSchedulers.mainThread()) // 鍦ㄤ富绾跨▼涓婅瀵�
-                                    .subscribe(() -> {
-                                        // 鎻掑叆鎴愬姛
-                                        Log.i("mWebView", "鏁版嵁鎻掑叆鎴愬姛");
-                                    }, throwable -> {
-                                        // 鎻掑叆澶辫触
-                                        Log.e("mWebView", "鏁版嵁鎻掑叆澶辫触: " + throwable.getMessage());
-                                    });
+                            
+                            // 浣跨敤 CompositeDisposable 绠$悊鏁版嵁搴撴彃鍏ユ搷浣�
+                            compositeDisposable.add(
+                                DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).divideDao().insertAll(divideBeans)
+                                    .subscribeOn(Schedulers.io())
+                                    .observeOn(AndroidSchedulers.mainThread())
+                                    .subscribe(
+                                        () -> {
+                                            // 鎻掑叆鎴愬姛
+                                            Log.i("mWebView", "鏁版嵁鎻掑叆鎴愬姛");
+                                        },
+                                        throwable -> {
+                                            // 鎻掑叆澶辫触
+                                            Log.e("mWebView", "鏁版嵁鎻掑叆澶辫触: " + throwable.getMessage());
+                                        }
+                                    )
+                            );
                         }
-
-
                     } else {
                         ToastUtil.showToastLong(MapFragment.this.getContext(), t.getMsg());
                     }
@@ -946,7 +1036,6 @@
                     CrashReport.postCatchedException(e);
                 }
             }
-
         });
     }
 
@@ -1031,6 +1120,10 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
+        // 娓呯悊鎵�鏈夎闃�
+        if (compositeDisposable != null && !compositeDisposable.isDisposed()) {
+            compositeDisposable.dispose();
+        }
         LiveEventBus.get(CommonKeyName.locationData).removeObserver(locationObserver);
         if (mWebView != null) {
             mWebView.destroy();
diff --git a/app/src/main/java/com/dayu/pipirrapp/fragment/MyFragment.java b/app/src/main/java/com/dayu/pipirrapp/fragment/MyFragment.java
index 085b0f1..4f63ea2 100644
--- a/app/src/main/java/com/dayu/pipirrapp/fragment/MyFragment.java
+++ b/app/src/main/java/com/dayu/pipirrapp/fragment/MyFragment.java
@@ -24,6 +24,7 @@
 import com.dayu.pipirrapp.utils.ToastUtil;
 import com.dayu.pipirrapp.view.ConfirmDialog;
 import com.dayu.pipirrapp.view.TipUtil;
+import com.jeremyliao.liveeventbus.LiveEventBus;
 
 /**
  * author: zuo
@@ -94,6 +95,10 @@
             Intent intent = new Intent(MyFragment.this.getContext(), IssueListActivity.class);
             MyFragment.this.getActivity().startActivity(intent);
         });
+        binding.refreshDataTV.setOnClickListener(v->{
+            // 鍙戦�佸埛鏂颁簨浠堕�氱煡MapFragment鍒锋柊鏁版嵁
+            LiveEventBus.get(CommonKeyName.refreshData).post(true);
+        });
     }
 
     private void initData() {
diff --git a/app/src/main/java/com/dayu/pipirrapp/net/Constants.java b/app/src/main/java/com/dayu/pipirrapp/net/Constants.java
index b892373..d0ae793 100644
--- a/app/src/main/java/com/dayu/pipirrapp/net/Constants.java
+++ b/app/src/main/java/com/dayu/pipirrapp/net/Constants.java
@@ -7,8 +7,8 @@
  */
 public class Constants {
     //    public static final String BASE_URL = "http://192.168.10.52:8088";
-//    public static final String BASE_URL = "https://no253541tf71.vicp.fun";
-    public static final String BASE_URL = "http://192.168.40.166:54321";
+    public static final String BASE_URL = "https://no253541tf71.vicp.fun";
+//    public static final String BASE_URL = "http://192.168.40.166:54321";
     //public static final String BASE_URL = "http://fve2iz.natappfree.cc";
     public static final String BASE_UPLOAD_FILE_URL = BASE_URL;
     /**
diff --git a/app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java b/app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java
index 6866ddf..1adf529 100644
--- a/app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java
+++ b/app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java
@@ -29,4 +29,8 @@
     //鍒涘缓閫氱煡
     public final static String CreateNotification="CreateNotification";
 
+    /**
+     * 鍒锋柊鏁版嵁浜嬩欢
+     */
+    public static final String refreshData = "refreshData";
 }
diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml
index 715b846..e0de4e9 100644
--- a/app/src/main/res/layout/fragment_map.xml
+++ b/app/src/main/res/layout/fragment_map.xml
@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <!--    <com.github.lzyzsd.jsbridge.BridgeWebView-->
     <!--        android:id="@+id/webView"-->
@@ -105,7 +106,21 @@
         android:textColor="@color/white"
         android:textSize="18sp" />
 
-
+    <com.example.expand_button.ExpandButton
+        android:id="@+id/expandButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:background="@drawable/ic_green_bg"
+        android:layout_alignParentRight="true"
+        android:layout_marginTop="180dp"
+        android:textSize="18sp"
+        android:layout_marginRight="15dp"
+        android:textColor="@color/white"
+        app:letterSpacing="10dp"
+        app:expandedText="鐐瑰嚮灞曞紑"
+        app:collapsedText="鐐�"
+        app:animDuration="300" />
     <RelativeLayout
         android:id="@+id/pointRL"
         android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/fragment_my.xml b/app/src/main/res/layout/fragment_my.xml
index 636d209..6478c63 100644
--- a/app/src/main/res/layout/fragment_my.xml
+++ b/app/src/main/res/layout/fragment_my.xml
@@ -131,12 +131,37 @@
             android:layout_marginRight="15dp"
             android:src="@drawable/ic_right" />
     </RelativeLayout>
+    <RelativeLayout
+        android:id="@+id/refreshDataRL"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/item_height"
+        android:layout_below="@+id/passwordRL"
+        android:layout_marginTop="1dp">
 
+        <TextView
+            android:id="@+id/refreshDataTV"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/item_height"
+            android:background="@color/white"
+            android:gravity="center_vertical"
+            android:paddingLeft="30dp"
+            android:text="鏇存柊鏈湴鏁版嵁"
+            android:textColor="@color/black"
+            android:textSize="@dimen/my_item_text_size" />
+
+        <ImageView
+            android:layout_width="25dp"
+            android:layout_height="25dp"
+            android:layout_alignParentRight="true"
+            android:layout_centerVertical="true"
+            android:layout_marginRight="15dp"
+            android:src="@drawable/ic_right" />
+    </RelativeLayout>
     <RelativeLayout
         android:id="@+id/cleanDataRL"
         android:layout_width="match_parent"
         android:layout_height="@dimen/item_height"
-        android:layout_below="@+id/passwordRL"
+        android:layout_below="@+id/refreshDataRL"
         android:layout_marginTop="1dp">
 
         <TextView
diff --git a/expand_button/src/main/java/com/example/expand_button/ExpandButton.kt b/expand_button/src/main/java/com/example/expand_button/ExpandButton.kt
index 841ce47..c024329 100644
--- a/expand_button/src/main/java/com/example/expand_button/ExpandButton.kt
+++ b/expand_button/src/main/java/com/example/expand_button/ExpandButton.kt
@@ -1,14 +1,319 @@
 package com.example.expand_button
 
-class ExpandButton {
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.style.ClickableSpan
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.content.ContextCompat
 
+/**
+ * 鍙睍寮�鐨勬寜閽帶浠�
+ * 鍒濆鐘舵�佹樉绀哄崟涓瓧绗︼紝鐐瑰嚮鍚庡睍寮�鏄剧ず瀹屾暣鏂囧瓧
+ * 灞曞紑鍚庣殑姣忎釜瀛楃閮藉彲浠ュ崟鐙偣鍑�
+ */
+class ExpandButton @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : AppCompatTextView(context, attrs, defStyleAttr) {
 
+    // 灞曞紑鏃舵樉绀虹殑瀹屾暣鏂囧瓧
+    private var expandedText: String = ""
+    // 鏀惰捣鏃舵樉绀虹殑鍗曚釜瀛楃
+    private var collapsedText: String = ""
+    // 褰撳墠鏄惁澶勪簬灞曞紑鐘舵��
+    private var isExpanded: Boolean = false
+    // 鍔ㄧ敾鎸佺画鏃堕棿锛岄粯璁�300姣
+    private var animationDuration: Long = 300
+    // 瀛楃鐐瑰嚮浜嬩欢鐩戝惉鍣�
+    private var onCharClickListener: ((Char, Int) -> Unit)? = null
+    // 瀛楅棿璺濓紝榛樿涓�2dp
+    private var customLetterSpacing: Float = context.resources.displayMetrics.density * 2
 
+    // 涓夎褰㈠浘鏍�
+    private val triangleDrawable: Drawable = ContextCompat.getDrawable(
+        context,
+        R.drawable.ic_triangle
+    )!!.mutate()
 
+    // 鍥炬爣鏃嬭浆瑙掑害
+    private var triangleRotation = 0f
 
+    // 涓夎褰㈠浘鏍囦笌鏂囧瓧鐨勯棿璺濓紝榛樿涓�8dp
+    private var triangleMargin: Float = 3 * context.resources.displayMetrics.density
 
+    init {
+        // 璇诲彇鑷畾涔夊睘鎬�
+        context.theme.obtainStyledAttributes(
+            attrs,
+            R.styleable.ExpandButton,
+            defStyleAttr,
+            0
+        ).apply {
+            try {
+                customLetterSpacing = getDimension(R.styleable.ExpandButton_letterSpacing, customLetterSpacing)
+                expandedText = getString(R.styleable.ExpandButton_expandedText) ?: ""
+                collapsedText = getString(R.styleable.ExpandButton_collapsedText) ?: ""
+                animationDuration = getInteger(R.styleable.ExpandButton_animDuration, 300).toLong()
+                triangleMargin = getDimension(R.styleable.ExpandButton_triangleMargin, triangleMargin)
+            } finally {
+                recycle()
+            }
+        }
 
+        // 璁剧疆鍒濆鏂囨湰鍜屽搴�
+        if (collapsedText.isNotEmpty()) {
+            text = collapsedText
+            post {
+                // 纭繚鍒濆瀹藉害涓烘敹璧风姸鎬佺殑瀹藉害
+                layoutParams = layoutParams.apply {
+                    width = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
+                }
+            }
+        }
 
+        // 璁剧疆鏂囨湰鍙偣鍑伙紝浠呭湪鏀惰捣鐘舵�佹椂鍝嶅簲鐐瑰嚮灞曞紑
+        setOnClickListener {
+            if (!isExpanded) {
+                toggleExpand()
+            }
+        }
 
+        // 娣诲姞瑙︽懜浜嬩欢澶勭悊
+        setOnTouchListener { _, event ->
+            when (event.action) {
+                MotionEvent.ACTION_DOWN -> {
+                    // 妫�鏌ョ偣鍑绘槸鍚﹀湪涓夎褰㈠浘鏍囧尯鍩熷唴
+                    if (isClickOnTriangle(event.x)) {
+                        toggleExpand()
+                        return@setOnTouchListener true
+                    }
+                }
+            }
+            false
+        }
 
+        // 璁剧疆宸﹁竟璺濓紝涓哄浘鏍囩暀鍑虹┖闂�
+        compoundDrawablePadding = triangleMargin.toInt()
+        setPadding(
+            (16 * context.resources.displayMetrics.density + triangleMargin).toInt(), // 宸﹁竟璺濆鍔狅紝涓哄浘鏍囩暀绌洪棿
+            paddingTop,
+            paddingRight,
+            paddingBottom
+        )
+
+        // 璁剧疆鍗曡鏄剧ず锛岄槻姝㈤珮搴﹀彉鍖�
+        maxLines = 1
+        isSingleLine = true
+        
+        // 璁剧疆鏂囧瓧鍨傜洿灞呬腑
+        gravity = Gravity.CENTER_VERTICAL
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        // 淇濆瓨鐢诲竷鐘舵��
+        canvas.save()
+        
+        // 璁$畻鍥炬爣浣嶇疆
+        val iconSize = triangleDrawable.intrinsicWidth
+        val iconLeft = paddingLeft - iconSize - compoundDrawablePadding
+        val iconTop = (height - iconSize) / 2
+        
+        // 璁剧疆鍥炬爣杈圭晫
+        triangleDrawable.setBounds(
+            iconLeft,
+            iconTop,
+            iconLeft + iconSize,
+            iconTop + iconSize
+        )
+        
+        // 鏃嬭浆鐢诲竷
+        canvas.rotate(
+            triangleRotation,
+            (iconLeft + iconSize / 2).toFloat(),
+            (iconTop + iconSize / 2).toFloat()
+        )
+        
+        // 缁樺埗鍥炬爣
+        triangleDrawable.draw(canvas)
+        
+        // 鎭㈠鐢诲竷鐘舵��
+        canvas.restore()
+        
+        super.onDraw(canvas)
+    }
+
+    /**
+     * 璁剧疆瀛楅棿璺�
+     * @param spacing 闂磋窛鍊硷紙鍍忕礌锛�
+     */
+    fun setCustomLetterSpacing(spacing: Float) {
+        this.customLetterSpacing = spacing
+        if (isExpanded) {
+            setExpandedClickableText()
+        }
+    }
+
+    /**
+     * 璁剧疆灞曞紑鍜屾敹璧锋椂鏄剧ず鐨勬枃瀛�
+     * @param expanded 灞曞紑鏃舵樉绀虹殑瀹屾暣鏂囧瓧
+     * @param collapsed 鏀惰捣鏃舵樉绀虹殑鍗曚釜瀛楃
+     */
+    fun setExpandText(expanded: String, collapsed: String) {
+        this.expandedText = expanded
+        this.collapsedText = collapsed
+        text = collapsedText
+    }
+
+    /**
+     * 璁剧疆鍗曚釜瀛楃鐐瑰嚮鐩戝惉鍣�
+     * @param listener 鐐瑰嚮鍥炶皟锛屽弬鏁颁负琚偣鍑荤殑瀛楃鍜屼綅缃�
+     *                char: 琚偣鍑荤殑瀛楃
+     *                position: 瀛楃鍦ㄦ枃鏈腑鐨勪綅缃紙浠�0寮�濮嬶級
+     */
+    fun setOnCharClickListener(listener: (char: Char, position: Int) -> Unit) {
+        this.onCharClickListener = listener
+    }
+
+    /**
+     * 璁剧疆灞曞紑/鏀惰捣鍔ㄧ敾鐨勬寔缁椂闂�
+     * @param duration 鍔ㄧ敾鎸佺画鏃堕棿锛堟绉掞級
+     */
+    fun setAnimationDuration(duration: Long) {
+        this.animationDuration = duration
+    }
+
+    /**
+     * 鍒囨崲灞曞紑/鏀惰捣鐘舵��
+     * 浣跨敤ValueAnimator瀹炵幇瀹藉害鍔ㄧ敾鍜屽浘鏍囨棆杞�
+     */
+    private fun toggleExpand() {
+        isExpanded = !isExpanded
+
+        // 璁$畻鏀惰捣鍜屽睍寮�鐘舵�佺殑瀹藉害
+        val collapsedWidth = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
+        val expandedWidth = calculateExpandedWidth()
+
+        // 鍒涘缓瀹藉害鍔ㄧ敾
+        ValueAnimator.ofInt(
+            if (isExpanded) collapsedWidth else expandedWidth,
+            if (isExpanded) expandedWidth else collapsedWidth
+        ).apply {
+            duration = animationDuration
+            addUpdateListener { animator ->
+                layoutParams = layoutParams.apply {
+                    width = animator.animatedValue as Int
+                }
+                requestLayout()
+            }
+            start()
+        }
+
+        // 鍒涘缓鍥炬爣鏃嬭浆鍔ㄧ敾
+        ValueAnimator.ofFloat(
+            if (isExpanded) 0f else 180f,
+            if (isExpanded) 180f else 0f
+        ).apply {
+            duration = animationDuration
+            addUpdateListener { animator ->
+                triangleRotation = animator.animatedValue as Float
+                invalidate() // 閲嶇粯浠ユ洿鏂板浘鏍囨棆杞�
+            }
+            start()
+        }
+
+        // 鏇存柊鏂囨湰
+        if (isExpanded) {
+            setExpandedClickableText()
+        } else {
+            text = collapsedText
+        }
+    }
+
+    /**
+     * 璁$畻灞曞紑鍚庣殑鎬诲搴�
+     */
+    private fun calculateExpandedWidth(): Int {
+        val spaceWidth = paint.measureText(" ") * (customLetterSpacing / 10)
+        // 璁$畻鎵�鏈夊瓧绗︾殑鎬诲搴�
+        val textWidth = expandedText.fold(0f) { acc, char ->
+            acc + paint.measureText(char.toString())
+        }
+        // 璁$畻闂磋窛鐨勬�诲搴︼紙瀛楃鏁伴噺鍑�1涓棿璺濓級
+        val spacesWidth = spaceWidth * (expandedText.length - 1)
+        return (textWidth + spacesWidth).toInt() + paddingLeft + paddingRight
+    }
+
+    /**
+     * 璁剧疆灞曞紑鍚庣殑鍙偣鍑绘枃鏈�
+     */
+    private fun setExpandedClickableText() {
+        val builder = SpannableStringBuilder()
+        expandedText.forEachIndexed { index, char ->
+            // 娣诲姞瀛楃
+            builder.append(char)
+            
+            // 涓哄瓧绗﹁缃偣鍑讳簨浠�
+            val clickableSpan = object : ClickableSpan() {
+                override fun onClick(view: View) {
+                    onCharClickListener?.invoke(char, index)
+                }
+
+                override fun updateDrawState(ds: android.text.TextPaint) {
+                    super.updateDrawState(ds)
+                    // 绉婚櫎涓嬪垝绾�
+                    ds.isUnderlineText = false
+                    // 淇濇寔鍘熷鏂囧瓧棰滆壊
+                    ds.color = currentTextColor
+                }
+            }
+            builder.setSpan(
+                clickableSpan,
+                builder.length - 1,
+                builder.length,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+            )
+            
+            // 鍙湪闈炴渶鍚庝竴涓瓧绗﹀悗娣诲姞绌烘牸浣滀负闂磋窛
+            if (index < expandedText.length - 1) {
+                builder.append(" ".repeat((customLetterSpacing / 10).toInt()))
+            }
+        }
+        
+        text = builder
+        // 鍚敤LinkMovementMethod浠ュ搷搴擟lickableSpan鐨勭偣鍑讳簨浠�
+        movementMethod = android.text.method.LinkMovementMethod.getInstance()
+    }
+
+    /**
+     * 鍒ゆ柇鐐瑰嚮鏄惁鍦ㄤ笁瑙掑舰鍥炬爣鍖哄煙鍐�
+     */
+    private fun isClickOnTriangle(x: Float): Boolean {
+        val iconSize = triangleDrawable.intrinsicWidth
+        val iconLeft = paddingLeft - iconSize - compoundDrawablePadding
+        return x <= paddingLeft && x >= iconLeft
+    }
+
+    /**
+     * 璁剧疆涓夎褰㈠浘鏍囦笌鏂囧瓧鐨勯棿璺�
+     * @param margin 闂磋窛鍊硷紙鍍忕礌锛�
+     */
+    fun setTriangleMargin(margin: Float) {
+        this.triangleMargin = margin
+        compoundDrawablePadding = margin.toInt()
+        setPadding(
+            (16 * context.resources.displayMetrics.density + margin).toInt(),
+            paddingTop,
+            paddingRight,
+            paddingBottom
+        )
+    }
 }
\ No newline at end of file
diff --git a/expand_button/src/main/res/drawable/ic_triangle.xml b/expand_button/src/main/res/drawable/ic_triangle.xml
new file mode 100644
index 0000000..86a68e8
--- /dev/null
+++ b/expand_button/src/main/res/drawable/ic_triangle.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+    <path
+        android:pathData="M19,12L31,24L19,36"
+        android:strokeLineJoin="round"
+        android:strokeWidth="4"
+        android:fillColor="#00000000"
+        android:strokeColor="#757575"
+        android:strokeLineCap="round"/>
+</vector> 
\ No newline at end of file
diff --git a/expand_button/src/main/res/values/attrs.xml b/expand_button/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..1d34c70
--- /dev/null
+++ b/expand_button/src/main/res/values/attrs.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="ExpandButton">
+        <!-- 瀛楅棿璺� -->
+        <attr name="letterSpacing" format="dimension"/>
+        <!-- 灞曞紑鏃舵樉绀虹殑鏂囧瓧 -->
+        <attr name="expandedText" format="string"/>
+        <!-- 鏀惰捣鏃舵樉绀虹殑鏂囧瓧 -->
+        <attr name="collapsedText" format="string"/>
+        <!-- 鍔ㄧ敾鏃堕暱 -->
+        <attr name="animDuration" format="integer"/>
+        <!-- 涓夎褰㈠浘鏍囦笌鏂囧瓧鐨勯棿璺� -->
+        <attr name="triangleMargin" format="dimension"/>
+    </declare-styleable>
+</resources> 
\ No newline at end of file

--
Gitblit v1.8.0