管灌系统巡查员智能手机App
1.添加显示隐藏取水口、分水房功能
2.完善图例自定义控件功能和显示
3.处理工单添加选择时间功能
11个文件已修改
4个文件已添加
845 ■■■■ 已修改文件
app/src/main/assets/js/map.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/MyApplication.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/utils/DateUtils.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/divide_home_blue.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/divide_home_unselected.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/marker_blue.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/marker_unselected.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_order_deal.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_map.xml 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/java/com/example/expand_button/ExpandButton.kt 416 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/res/values/attrs.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/assets/js/map.js
@@ -27,6 +27,12 @@
    let pipeLineList = [];
    let currentPipePath = [];
    // 存储所有取水口标记的数组
    let waterIntakeMarkers = [];
    // 存储所有分水房标记的数组
    let divideMarkers = [];
    // 将方法挂载到 window 上
    function mountMethodToWindow() {
        window.locationOverLay = locationOverLay;
@@ -46,6 +52,10 @@
        window.hideAllPipeLines = hideAllPipeLines;
        window.clearAllPipeLines = clearAllPipeLines;
        window.addPipeNetwork = addPipeNetwork;
        window.hideAllWaterIntakes = hideAllWaterIntakes;
        window.showAllWaterIntakes = showAllWaterIntakes;
        window.hideAllDivides = hideAllDivides;
        window.showAllDivides = showAllDivides;
    }
@@ -242,19 +252,26 @@
        let label = new T.Label({
            text: `<div style='position:absolute;left:-50%;transform: translateX(-50%);'>${name}<div>`,
            position: marker.getLngLat(),
            offset: new T.Point(0, 8), // 设置标注文字的位置
            opacity: 1, // 设置文本的显示不透明度(范围0-1)
            offset: new T.Point(0, 8),
            opacity: 1,
        });
        label.setBorderLine(0); // 设置文本的边框线宽
        label.setBackgroundColor("transparent"); // 设置文本的背景色(透明色)
        label.setBorderLine(0);
        label.setBackgroundColor("transparent");
        label.setFontColor("#FFFFFF");
        label.setFontSize(10);
        marker.label = label;
        if (isRed) {
            lastClickedMarker = marker;
        }
        // 将标记和标签存储到数组中
        waterIntakeMarkers.push({
            marker: marker,
            label: label
        });
        map.addOverLay(label);
        map.addOverLay(marker); // 将标注添加到地图中
        map.addOverLay(marker);
        return "addMarker加载成功 id:" + id
    }
    //更新位坐标
@@ -538,6 +555,13 @@
        if (isRed) {
            lastClickedMarker = marker;
        }
        // 将分水房标记和标签存储到数组中
        divideMarkers.push({
            marker: marker,
            label: label
        });
        map.addOverLay(label);
        map.addOverLay(marker); // 将标注添加到地图中
        return "addMarker加载成功 id:" + id
@@ -577,9 +601,44 @@
        window.Android.showDivideDetail(data);
    }
    /**
     * 隐藏所有取水口标记
     */
    function hideAllWaterIntakes() {
        waterIntakeMarkers.forEach(item => {
            map.removeOverLay(item.marker);
            map.removeOverLay(item.label);
        });
    }
    /**
     * 显示所有取水口标记
     */
    function showAllWaterIntakes() {
        waterIntakeMarkers.forEach(item => {
            map.addOverLay(item.marker);
            map.addOverLay(item.label);
        });
    }
    /**
     * 隐藏所有分水房标记
     */
    function hideAllDivides() {
        divideMarkers.forEach(item => {
            map.removeOverLay(item.marker);
            map.removeOverLay(item.label);
        });
    }
    /**
     * 显示所有分水房标记
     */
    function showAllDivides() {
        divideMarkers.forEach(item => {
            map.addOverLay(item.marker);
            map.addOverLay(item.label);
        });
    }
})();
app/src/main/java/com/dayu/pipirrapp/MyApplication.java
@@ -32,7 +32,7 @@
//        JPushInterface.setDebugMode(true);
//        JPushInterface.init(this);
        CrashReport.initCrashReport(getApplicationContext(), "3d4bcf7046", false);
        CrashReport.initCrashReport(getApplicationContext(), "f6dea280a2", false);
//        // 设置全局的UncaughtExceptionHandler
//        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java
@@ -23,13 +23,11 @@
import com.dayu.pipirrapp.bean.net.AddProcessingRequest;
import com.dayu.pipirrapp.bean.net.InsectionResult;
import com.dayu.pipirrapp.bean.net.UplodFileState;
import com.dayu.pipirrapp.dao.DaoSingleton;
import com.dayu.pipirrapp.databinding.ActivityOrderDealBinding;
import com.dayu.pipirrapp.fragment.OrderFragment;
import com.dayu.pipirrapp.net.ApiManager;
import com.dayu.pipirrapp.net.BaseResponse;
import com.dayu.pipirrapp.net.subscribers.SubscriberListener;
import com.dayu.pipirrapp.net.upload.UploadFileListener;
import com.dayu.pipirrapp.tool.FileUploadUtils;
import com.dayu.pipirrapp.tool.FullyGridLayoutManager;
import com.dayu.pipirrapp.tool.GlideEngine;
@@ -58,10 +56,8 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import retrofit2.Call;
@@ -87,6 +83,7 @@
    Map<String, UplodFileState> uplodFileStates = new HashMap<>();
    String workOrderId;
    LatLonBean latLonBean;
    String strCompleteTime;
    /**
     * 定位监听
@@ -124,13 +121,15 @@
            new CardDatePickerDialog.Builder(this)
                    .setTitle("选择处理时间")
                    .setOnChoose("确定", aLong -> {
                        //aLong  = millisecond
                        //aLong = millisecond
                        strCompleteTime = com.dayu.pipirrapp.utils.DateUtils.formatTimestamp(aLong);
                        binding.timeData.setText(strCompleteTime);
                        return null;
                    })
                    .showBackNow(true)
                    .setDefaultTime(time)
                    .setMaxTime(time)
                    .setMinTime(time - 365L * 24 * 60 * 60 * 1000) // 设置最小时间为一年前
                    .setDisplayType(list)
                    .build().show();
        });
@@ -306,7 +305,7 @@
        result.setContent(binding.contentET.getText().toString());
        result.setInspectorId(MyApplication.myApplication.userId);
        result.setWorkOrderId(workOrderId);
        result.setCompleteTime(com.dayu.pipirrapp.utils.DateUtils.getNowDateToMMStr());
        result.setCompleteTime(strCompleteTime);
        if (latLonBean != null) {
            result.setLat(String.valueOf(latLonBean.getLatitude()));
            result.setLng(String.valueOf(latLonBean.getLongitude()));
app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java
@@ -17,7 +17,7 @@
import io.reactivex.rxjava3.core.Maybe;
/**
 * DivideDao -
 * DivideDao -分水房
 *
 * @author zuoxiao
 * @version 1.0
@@ -52,6 +52,6 @@
    @Query("select  * from DivideBean")
    Single<List<DivideBean>> findAllToSingle();
    @Query("SELECT * FROM divide")
    @Query("SELECT * FROM DivideBean")
    Maybe<List<DivideBean>> getAll();  // 改为返回Maybe<List<DivideBean>>
}
app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java
@@ -19,7 +19,7 @@
 * author: zuo
 * Date: 2024-09-30
 * Time: 14:39
 * 备注:
 * 备注:取水口
 */
@Dao
public interface MarkerDao {
app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java
@@ -5,9 +5,11 @@
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -20,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Observer;
import com.dayu.pipirrapp.MyApplication;
@@ -62,6 +65,7 @@
import com.dayu.pipirrapp.utils.WebViewUtils;
import com.dayu.pipirrapp.view.ConfirmDialog;
import com.dayu.pipirrapp.view.TipUtil;
import com.example.expand_button.ExpandButton;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
@@ -80,6 +84,7 @@
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Triple;
/**
 * author: zuo
@@ -178,69 +183,69 @@
    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();
                }, () -> {
                    // 当Maybe为空时调用
                    getCenterPoint();
                })
                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();
                        }, () -> {
                            // 当Maybe为空时调用
                            getCenterPoint();
                        })
        );
        // 异步加载取水口数据
        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();
                }, () -> {
                    // 当Maybe为空时调用
                    getMarkerData();
                })
                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();
                        }, () -> {
                            // 当Maybe为空时调用
                            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();
                }, () -> {
                    // 当Maybe为空时调用
                    getDivideList();
                })
                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();
                        }, () -> {
                            // 当Maybe为空时调用
                            getDivideList();
                        })
        );
    }
@@ -370,19 +375,19 @@
                        // 使用 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());
                                    }
                                )
                                DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).markerDao().insertAll(markerBeans)
                                        .subscribeOn(Schedulers.io())
                                        .observeOn(AndroidSchedulers.mainThread())
                                        .subscribe(
                                                () -> {
                                                    // 插入成功
                                                    Log.i("mWebView", "数据插入成功");
                                                },
                                                throwable -> {
                                                    // 插入失败
                                                    Log.e("mWebView", "数据插入失败: " + throwable.getMessage());
                                                }
                                        )
                        );
                    }
@@ -432,8 +437,27 @@
            Intent issue = new Intent(MapFragment.this.getActivity(), AddIssueActivity.class);
            MapFragment.this.getActivity().startActivity(issue);
        });
        binding.expandButton.setLegendsArray(new Triple<>(
                        ContextCompat.getDrawable(requireContext(), R.drawable.marker_blue),
                        ContextCompat.getDrawable(requireContext(), R.drawable.marker_unselected),
                        "取水口"
                ),
                new Triple<>(
                        ContextCompat.getDrawable(requireContext(), R.drawable.divide_home_blue),
                        ContextCompat.getDrawable(requireContext(), R.drawable.divide_home_unselected),
                        "分水房"
                ));
        binding.expandButton.setOnLegendItemClickListener((position, isSelected) -> {
            switch (position) {
                case 0:
                    showMarkers(isSelected);
                    break;
                case 1:
                    showDivideMarkers(isSelected);
                    break;
            }
        });
    }
    /**
@@ -1010,22 +1034,22 @@
                                setMapDivide(divideBean);
                                divideBeans.add(divideBean);
                            }
                            // 使用 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());
                                        }
                                    )
                                    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 {
@@ -1157,4 +1181,36 @@
            mWebView.restoreState(savedInstanceState);
        }
    }
    /**
     * 显示或隐藏地图上的取水口
     *
     * @param isShow
     */
    private void showMarkers(boolean isShow) {
        if (isShow) {
            mWebView.evaluateJavascript("javascript:showAllWaterIntakes()", value -> {
            });
        } else {
            mWebView.evaluateJavascript("javascript:hideAllWaterIntakes()", value -> {
            });
        }
    }
    /**
     * 显示或隐藏地图上的分水房
     *
     * @param isShow
     */
    private void showDivideMarkers(boolean isShow) {
        if (isShow) {
            mWebView.evaluateJavascript("javascript:showAllDivides()", value -> {
            });
        } else {
            mWebView.evaluateJavascript("javascript:hideAllDivides()", value -> {
            });
        }
    }
}
app/src/main/java/com/dayu/pipirrapp/utils/DateUtils.java
@@ -31,15 +31,25 @@
    /**
     * 返回统一格式的当前时间截止到分钟
     *
     * @return yyyy-MM-dd HH:mm:ss
     * @return yyyy-MM-dd HH:mm
     */
    public static String getNowDateToMMStr() {
        // 当前时间
        Date date = new Date();
        // 创建 SimpleDateFormat 对象,设置日期格式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
        // 格式化当前时间为字符串
        return sdf.format(date);
    }
    /**
     * 将时间戳转换为指定格式的字符串
     * @param timestamp 时间戳
     * @return 格式化后的时间字符串 (yyyy-MM-dd HH:mm)
     */
    public static String formatTimestamp(long timestamp) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
        return sdf.format(new Date(timestamp));
    }
}
app/src/main/res/drawable/divide_home_blue.xml
New file
@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="48"
    android:viewportHeight="48">
  <path
      android:pathData="M9,18V42H39V18L24,6L9,18Z"
      android:strokeLineJoin="round"
      android:strokeWidth="4"
      android:fillColor="#00000000"
      android:strokeColor="#1890FF"
      android:strokeLineCap="round"/>
  <path
      android:pathData="M19,29V42H29V29H19Z"
      android:strokeLineJoin="round"
      android:strokeWidth="4"
      android:fillColor="#00000000"
      android:strokeColor="#1890FF"/>
  <path
      android:pathData="M9,42H39"
      android:strokeWidth="4"
      android:fillColor="#00000000"
      android:strokeColor="#1890FF"
      android:strokeLineCap="round"/>
</vector>
app/src/main/res/drawable/divide_home_unselected.xml
New file
@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="48"
    android:viewportHeight="48">
  <path
      android:pathData="M9,18V42H39V18L24,6L9,18Z"
      android:strokeLineJoin="round"
      android:strokeWidth="4"
      android:fillColor="#00000000"
      android:strokeColor="#757575"
      android:strokeLineCap="round"/>
  <path
      android:pathData="M19,29V42H29V29H19Z"
      android:strokeLineJoin="round"
      android:strokeWidth="4"
      android:fillColor="#00000000"
      android:strokeColor="#757575"/>
  <path
      android:pathData="M9,42H39"
      android:strokeWidth="4"
      android:fillColor="#00000000"
      android:strokeColor="#757575"
      android:strokeLineCap="round"/>
</vector>
app/src/main/res/drawable/marker_blue.xml
New file
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <path
      android:pathData="M12,2L12,2C8.13,2 5,5.13 5,9c0,1.74 0.5,3.37 1.41,4.84c0.95,1.54 2.2,2.86 3.16,4.4c0.47,0.75 0.81,1.45 1.17,2.26C11,21.05 11.21,22 12,22h0c0.79,0 1,-0.95 1.25,-1.5c0.37,-0.81 0.7,-1.51 1.17,-2.26c0.96,-1.53 2.21,-2.85 3.16,-4.4C18.5,12.37 19,10.74 19,9C19,5.13 15.87,2 12,2zM12,11.75c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,11.75 12,11.75z"
      android:fillColor="#1890FF"/>
</vector>
app/src/main/res/drawable/marker_unselected.xml
New file
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <path
      android:pathData="M12,2L12,2C8.13,2 5,5.13 5,9c0,1.74 0.5,3.37 1.41,4.84c0.95,1.54 2.2,2.86 3.16,4.4c0.47,0.75 0.81,1.45 1.17,2.26C11,21.05 11.21,22 12,22h0c0.79,0 1,-0.95 1.25,-1.5c0.37,-0.81 0.7,-1.51 1.17,-2.26c0.96,-1.53 2.21,-2.85 3.16,-4.4C18.5,12.37 19,10.74 19,9C19,5.13 15.87,2 12,2zM12,11.75c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,11.75 12,11.75z"
      android:fillColor="#757575"/>
</vector>
app/src/main/res/layout/activity_order_deal.xml
@@ -62,7 +62,15 @@
                    android:text="反馈时间:"
                    android:textColor="@color/black"
                    android:textSize="@dimen/order_detail_button_size" />
                <TextView
                    android:id="@+id/timeData"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_toEndOf="@+id/timeTV"
                    android:layout_marginLeft="10dp"
                    android:textColor="@color/black"
                    android:textSize="@dimen/order_detail_button_size" />
                <ImageView
                    android:layout_width="25dp"
                    android:layout_height="25dp"
app/src/main/res/layout/fragment_map.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    android:layout_height="match_parent">
    <!--    <com.github.lzyzsd.jsbridge.BridgeWebView-->
    <!--        android:id="@+id/webView"-->
@@ -110,17 +110,18 @@
        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:background="@drawable/ic_green_bg"
        android:padding="10dp"
        android:textColor="@color/white"
        android:textSize="18sp"
        app:animDuration="300"
        app:collapsedText="例"
        app:letterSpacing="10dp"
        app:expandedText="点击展开"
        app:collapsedText="点"
        app:animDuration="300" />
        app:expandedTextSize="12sp"/>
    <RelativeLayout
        android:id="@+id/pointRL"
        android:layout_width="match_parent"
expand_button/src/main/java/com/example/expand_button/ExpandButton.kt
@@ -25,10 +25,23 @@
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
    // 修改属性
    private data class LegendItem(
        val selectedIcon: Drawable,
        val unselectedIcon: Drawable,
        val description: String,
        var isSelected: Boolean = true
    )
    private var legendItems: List<LegendItem> = listOf()
    private var itemSpacing: Float = context.resources.displayMetrics.density * 16 // 图例项之间的水平间距
    private var iconSize: Int = (24 * context.resources.displayMetrics.density).toInt() // 图标大小
    private var iconTextSpacing: Float = context.resources.displayMetrics.density * 4 // 图标和文字之间的垂直间距
    // 展开时显示的完整文字
    private var expandedText: String = ""
    private var expandedText: String = "标注点: 当前位置\n区域: 配送范围"
    // 收起时显示的单个字符
    private var collapsedText: String = ""
    private var collapsedText: String = "图例"
    // 当前是否处于展开状态
    private var isExpanded: Boolean = false
    // 动画持续时间,默认300毫秒
@@ -50,7 +63,33 @@
    // 三角形图标与文字的间距,默认为8dp
    private var triangleMargin: Float = 3 * context.resources.displayMetrics.density
    // 添加新属性
    private var textLines: List<String> = listOf()
    // 添加点击回调接口
    interface OnLegendItemClickListener {
        fun onLegendItemClick(position: Int, isSelected: Boolean)
    }
    private var legendItemClickListener: OnLegendItemClickListener? = null
    // 修改图例项的总高度计算
    private val legendItemHeight: Int
        get() = iconSize + iconTextSpacing.toInt() + paint.textSize.toInt() + paint.descent().toInt() - paint.ascent().toInt()
    // 添加展开后的字体大小属性
    private var expandedTextSize: Float = textSize
    // 添加一个变量保存默认字体大小
    private var defaultTextSize: Float = 0f
    // 添加一个属性定义三角形图标的点击区域扩展范围
    private val triangleClickPadding: Float = 15f * context.resources.displayMetrics.density // 20dp
    init {
        // 保存 XML 中设置的默认字体大小
        defaultTextSize = textSize
        // 读取自定义属性
        context.theme.obtainStyledAttributes(
            attrs,
@@ -60,10 +99,14 @@
        ).apply {
            try {
                customLetterSpacing = getDimension(R.styleable.ExpandButton_letterSpacing, customLetterSpacing)
                expandedText = getString(R.styleable.ExpandButton_expandedText) ?: ""
                collapsedText = getString(R.styleable.ExpandButton_collapsedText) ?: ""
                expandedText = getString(R.styleable.ExpandButton_expandedText) ?: "标注点: 当前位置\n区域: 配送范围"
                collapsedText = getString(R.styleable.ExpandButton_collapsedText) ?: "图例"
                animationDuration = getInteger(R.styleable.ExpandButton_animDuration, 300).toLong()
                triangleMargin = getDimension(R.styleable.ExpandButton_triangleMargin, triangleMargin)
                itemSpacing = getDimension(R.styleable.ExpandButton_itemSpacing, itemSpacing)
                iconSize = getDimension(R.styleable.ExpandButton_iconSize, iconSize.toFloat()).toInt()
                iconTextSpacing = getDimension(R.styleable.ExpandButton_iconTextSpacing, iconTextSpacing)
                expandedTextSize = getDimension(R.styleable.ExpandButton_expandedTextSize, defaultTextSize)
            } finally {
                recycle()
            }
@@ -80,26 +123,11 @@
            }
        }
        // 设置文本可点击,仅在收起状态时响应点击展开
        setOnClickListener {
            if (!isExpanded) {
                toggleExpand()
            }
        }
        // 添加触摸事件处理
        setOnTouchListener { _, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    // 检查点击是否在三角形图标区域内
                    if (isClickOnTriangle(event.x)) {
                        toggleExpand()
                        return@setOnTouchListener true
                    }
                }
            }
            false
        }
        // 修改触摸事件处理
        setOnTouchListener(null) // 移除原有的触摸监听器
        // 移除原有的点击监听器
        setOnClickListener(null)
        // 设置左边距,为图标留出空间
        compoundDrawablePadding = triangleMargin.toInt()
@@ -111,23 +139,107 @@
        )
        // 设置单行显示,防止高度变化
        maxLines = 1
        isSingleLine = true
        maxLines = if (isExpanded) Int.MAX_VALUE else 1
        isSingleLine = !isExpanded
        
        // 设置文字垂直居中
        gravity = Gravity.CENTER_VERTICAL
        // 设置默认的内边距
        val defaultPadding = (8 * context.resources.displayMetrics.density).toInt()
        setPadding(
            (16 * context.resources.displayMetrics.density + triangleMargin).toInt(), // 左边距增加,为图标留空间
            defaultPadding, // 上边距
            defaultPadding, // 右边距
            defaultPadding  // 下边距
        )
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (isExpanded) {
            // 展开状态下的高度计算
            val desiredHeight = legendItemHeight + paddingTop + paddingBottom
            setMeasuredDimension(measuredWidth, desiredHeight)
        }
    }
    override fun onDraw(canvas: Canvas) {
        // 保存画布状态
        // 绘制展开/收起图标
        drawTriangle(canvas)
        if (!isExpanded) {
            // 收起状态使用默认字体大小
            paint.textSize = defaultTextSize
            super.onDraw(canvas)
            return
        }
        // 展开状态使用展开后的字体大小
        paint.textSize = expandedTextSize
        // 计算所有图例项中最宽的宽度
        val maxWidth = legendItems.maxOf { item ->
            maxOf(paint.measureText(item.description), iconSize.toFloat())
        }
        // 计算总宽度(添加首尾的边距)
        val totalWidth = legendItems.size * maxWidth +
            (legendItems.size + 1) * itemSpacing  // 修改这里,添加一个额外的间距
        // 计算起始x坐标,使整体水平居中
        var x = (width - totalWidth) / 2 + itemSpacing  // 添加起始边距
        // 计算垂直居中的y坐标,考虑上下边距
        val centerY = height / 2f
        legendItems.forEachIndexed { index, item ->
            // 计算当前图例项的起始位置(移除index > 0的判断)
            val itemStartX = x
            // 计算图标的水平位置(居中于图例项)
            val iconLeft = itemStartX + (maxWidth - iconSize) / 2
            // 计算文字的位置
            val textWidth = paint.measureText(item.description)
            val textX = itemStartX + (maxWidth - textWidth) / 2
            // 绘制图标,根据选中状态选择不同的图标
            val iconTop = paddingTop + (height - legendItemHeight) / 2
            val currentIcon = if (item.isSelected) item.selectedIcon else item.unselectedIcon
            currentIcon.setBounds(
                iconLeft.toInt(),
                iconTop.toInt(),
                (iconLeft + iconSize).toInt(),
                (iconTop + iconSize).toInt()
            )
            currentIcon.draw(canvas)
            // 绘制文字,根据选中状态使用不同的颜色
            paint.color = if (item.isSelected)
                currentTextColor
            else
                0xFF999999.toInt() // 灰色
            val textY = iconTop + iconSize + iconTextSpacing - paint.ascent()
            canvas.drawText(item.description, textX, textY, paint)
            // 更新下一个图例项的起始位置
            x = itemStartX + maxWidth + itemSpacing
        }
        // 恢复画笔颜色
        paint.color = currentTextColor
    }
    // 将原来的onDraw中的三角形绘制逻辑提取出来
    private fun drawTriangle(canvas: Canvas) {
        canvas.save()
        
        // 计算图标位置
        val iconSize = triangleDrawable.intrinsicWidth
        val iconLeft = paddingLeft - iconSize - compoundDrawablePadding
        val iconTop = (height - iconSize) / 2
        
        // 设置图标边界
        triangleDrawable.setBounds(
            iconLeft,
            iconTop,
@@ -135,20 +247,15 @@
            iconTop + iconSize
        )
        
        // 旋转画布
        canvas.rotate(
            triangleRotation,
            (iconLeft + iconSize / 2).toFloat(),
            (iconTop + iconSize / 2).toFloat()
        )
        
        // 绘制图标
        triangleDrawable.draw(canvas)
        
        // 恢复画布状态
        canvas.restore()
        super.onDraw(canvas)
    }
    /**
@@ -193,13 +300,18 @@
    /**
     * 切换展开/收起状态
     * 使用ValueAnimator实现宽度动画和图标旋转
     */
    private fun toggleExpand() {
        isExpanded = !isExpanded
        // 计算收起和展开状态的宽度
        val collapsedWidth = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
        val collapsedWidth = run {
            paint.textSize = defaultTextSize
            val width = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
            paint.textSize = if (isExpanded) expandedTextSize else defaultTextSize
            width
        }
        val expandedWidth = calculateExpandedWidth()
        // 创建宽度动画
@@ -225,31 +337,46 @@
            duration = animationDuration
            addUpdateListener { animator ->
                triangleRotation = animator.animatedValue as Float
                invalidate() // 重绘以更新图标旋转
                invalidate()
            }
            start()
        }
        // 更新文本
        // 更新文本和字体大小
        if (isExpanded) {
            setExpandedClickableText()
            paint.textSize = expandedTextSize
        } else {
            text = collapsedText
            paint.textSize = defaultTextSize
            setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, defaultTextSize)
        }
        invalidate()
    }
    /**
     * 计算展开后的总宽度
     */
    private fun calculateExpandedWidth(): Int {
        val spaceWidth = paint.measureText(" ") * (customLetterSpacing / 10)
        // 计算所有字符的总宽度
        val textWidth = expandedText.fold(0f) { acc, char ->
            acc + paint.measureText(char.toString())
        // 临时保存当前字体大小
        val currentTextSize = paint.textSize
        // 设置为展开状态的字体大小
        paint.textSize = expandedTextSize
        try {
            // 计算所有图例项中最宽的宽度
            val maxWidth = legendItems.maxOf { item ->
                maxOf(paint.measureText(item.description), iconSize.toFloat())
            }
            // 计算总宽度 = 所有图例项的宽度 + 所有间距(包括首尾) + 左右内边距
            return (legendItems.size * maxWidth +
                (legendItems.size + 1) * itemSpacing +
                paddingLeft + paddingRight).toInt()
        } finally {
            // 恢复原来的字体大小
            paint.textSize = currentTextSize
        }
        // 计算间距的总宽度(字符数量减1个间距)
        val spacesWidth = spaceWidth * (expandedText.length - 1)
        return (textWidth + spacesWidth).toInt() + paddingLeft + paddingRight
    }
    /**
@@ -257,39 +384,76 @@
     */
    private fun setExpandedClickableText() {
        val builder = SpannableStringBuilder()
        expandedText.forEachIndexed { index, char ->
            // 添加字符
            builder.append(char)
        // 计算所有图例项中最宽的宽度
        val maxWidth = legendItems.maxOf { item ->
            maxOf(paint.measureText(item.description), iconSize.toFloat())
        }
        // 计算总宽度
        val totalWidth = legendItems.size * maxWidth +
            (legendItems.size - 1) * itemSpacing
        // 计算整体水平居中需要的起始空格
        val startPadding = ((width - totalWidth) / 2 / paint.measureText(" ")).toInt()
        if (startPadding > 0) {
            builder.append(" ".repeat(startPadding))
        }
        // 添加垂直空间,为图标预留位置
        val verticalSpaces = ((iconSize + iconTextSpacing) / paint.textSize).toInt()
        builder.append("\n".repeat(verticalSpaces))
        legendItems.forEachIndexed { index, item ->
            if (index > 0) {
                // 在图例项之间添加水平间距
                builder.append(" ".repeat((itemSpacing / paint.measureText(" ")).toInt()))
            }
            
            // 为字符设置点击事件
            // 计算水平居中所需的空格数
            val textWidth = paint.measureText(item.description)
            val paddingSpaces = ((maxWidth - textWidth) / 2 / paint.measureText(" ")).toInt()
            // 添加左侧空格实现居中
            if (paddingSpaces > 0) {
                builder.append(" ".repeat(paddingSpaces))
            }
            // 添加描述文本
            val startPosition = builder.length
            builder.append(item.description)
            // 添加右侧空格以确保宽度一致
            val remainingSpaces = ((maxWidth - textWidth) / paint.measureText(" ")).toInt() - paddingSpaces
            if (remainingSpaces > 0) {
                builder.append(" ".repeat(remainingSpaces))
            }
            // 为文字设置点击事件
            val clickableSpan = object : ClickableSpan() {
                override fun onClick(view: View) {
                    onCharClickListener?.invoke(char, index)
                    onCharClickListener?.invoke(item.description[0], 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,
                startPosition,
                startPosition + item.description.length,
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            )
            // 只在非最后一个字符后添加空格作为间距
            if (index < expandedText.length - 1) {
                builder.append(" ".repeat((customLetterSpacing / 10).toInt()))
            }
        }
        
        // 设置文本对齐方式为居中
        gravity = Gravity.CENTER
        text = builder
        // 启用LinkMovementMethod以响应ClickableSpan的点击事件
        movementMethod = android.text.method.LinkMovementMethod.getInstance()
    }
@@ -299,7 +463,10 @@
    private fun isClickOnTriangle(x: Float): Boolean {
        val iconSize = triangleDrawable.intrinsicWidth
        val iconLeft = paddingLeft - iconSize - compoundDrawablePadding
        return x <= paddingLeft && x >= iconLeft
        // 扩大点击区域:左右各增加 triangleClickPadding
        return x <= (paddingLeft + triangleClickPadding) &&
               x >= (iconLeft - triangleClickPadding)
    }
    /**
@@ -316,4 +483,129 @@
            paddingBottom
        )
    }
    /**
     * 设置图例内容
     */
    @JvmName("setLegendsList")
    fun setLegends(items: List<Triple<Drawable, Drawable, String>>) {
        legendItems = items.map { (selectedIcon, unselectedIcon, description) ->
            selectedIcon.setBounds(0, 0, iconSize, iconSize)
            unselectedIcon.setBounds(0, 0, iconSize, iconSize)
            LegendItem(selectedIcon, unselectedIcon, description)
        }
        if (!isExpanded) {
            text = collapsedText
        } else {
            invalidate()
        }
        requestLayout()
    }
    // 添加一个 Java 友好的方法
    @JvmName("setLegendsArray")
    fun setLegends(vararg items: Triple<Drawable, Drawable, String>) {
        setLegends(items.toList())
    }
    /**
     * 添加设置监听器的方法
     */
    fun setOnLegendItemClickListener(listener: OnLegendItemClickListener) {
        legendItemClickListener = listener
    }
    /**
     * 处理触摸事件
     */
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 检查点击是否在三角形图标区域内
                if (isClickOnTriangle(event.x)) {
                    toggleExpand()
                    return true
                }
                // 如果是展开状态,检查是否点击了图例项
                if (isExpanded) {
                    val clickedIndex = getClickedItemIndex(event.x, event.y)
                    if (clickedIndex != -1) {
                        toggleItemSelection(clickedIndex)
                        return true
                    }
                } else if (!isExpanded && event.x > paddingLeft) {
                    // 在收起状态下,点击非三角形区域也展开
                    toggleExpand()
                    return true
                }
            }
        }
        return super.onTouchEvent(event)
    }
    /**
     * 获取点击位置对应的图例项索引
     */
    private fun getClickedItemIndex(x: Float, y: Float): Int {
        if (!isExpanded) return -1
        val maxWidth = legendItems.maxOf { item ->
            maxOf(paint.measureText(item.description), iconSize.toFloat())
        }
        val totalWidth = legendItems.size * maxWidth +
            (legendItems.size - 1) * itemSpacing
        val startX = (width - totalWidth) / 2
        val iconTop = paddingTop + (height - legendItemHeight) / 2
        val iconBottom = iconTop + legendItemHeight
        // 检查垂直方向是否在图例项范围内
        if (y < iconTop || y > iconBottom) return -1
        // 检查水平方向点击的是哪个图例项
        legendItems.forEachIndexed { index, _ ->
            val itemStartX = startX + index * (maxWidth + itemSpacing)
            val itemEndX = itemStartX + maxWidth
            if (x >= itemStartX && x <= itemEndX) {
                return index
            }
        }
        return -1
    }
    /**
     * 切换图例项的选中状态
     */
    private fun toggleItemSelection(index: Int) {
        if (index < 0 || index >= legendItems.size) return
        legendItems[index].isSelected = !legendItems[index].isSelected
        legendItemClickListener?.onLegendItemClick(
            index,
            legendItems[index].isSelected
        )
        invalidate()
    }
    /**
     * 设置展开后的字体大小
     * @param size 字体大小(像素)
     */
    fun setExpandedTextSize(size: Float) {
        this.expandedTextSize = size
        if (isExpanded) {
            invalidate()
        }
    }
    /**
     * 设置展开后的字体大小(SP)
     * @param sp 字体大小(SP)
     */
    fun setExpandedTextSizeSp(sp: Float) {
        setExpandedTextSize(sp * context.resources.displayMetrics.scaledDensity)
    }
}
expand_button/src/main/res/values/attrs.xml
@@ -11,5 +11,11 @@
        <attr name="animDuration" format="integer"/>
        <!-- 三角形图标与文字的间距 -->
        <attr name="triangleMargin" format="dimension"/>
        <!-- 新增属性 -->
        <attr name="itemSpacing" format="dimension"/>
        <attr name="iconSize" format="dimension"/>
        <attr name="iconTextSpacing" format="dimension"/>
        <!-- 新增展开后的字体大小属性 -->
        <attr name="expandedTextSize" format="dimension"/>
    </declare-styleable>
</resources>