管灌系统巡查员智能手机App
1.web地图添加管网显示。
2.安卓原生添加管网信息的获取。
3.安卓原生添加管网信息的本地持久化(SQLite数据库)。
4.实现图例用户的选择状态按钮的持久化。
5.实现自定义搜索按钮的实现。
6.实现搜索后弹出界面的相关功能开发。
18个文件已添加
13个文件已修改
1个文件已删除
1679 ■■■■ 已修改文件
app/src/main/assets/img/location_blue.svg 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/assets/js/map.js 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/bean/db/PipeNetWorkDataBean.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/bean/db/PipeNetworkBean.java 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/bean/db/PipeNetworkDataCoordinateBean.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/bean/net/PipeNetworkResult.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/dao/PipeNetDao.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/db/converter/PipeNetworkConverter.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java 205 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/service/MyCommonService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/view/SearchResultDialog.java 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/ic_pipenetwork_line.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/ic_pipenetwork_line_unselected.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/location_marker_bg.xml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/walking_worker_animation.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/worker_frame1.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/worker_frame2.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_search_result.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_map.xml 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/item_search_result.xml 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/styles.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/java/com/example/expand_button/CustomSearchBar.kt 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/java/com/example/expand_button/ExpandButton.kt 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/java/com/example/expand_button/ExpandSearchView.kt 449 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/res/drawable/bg_search_bar.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/res/drawable/ic_search.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/res/layout/layout_search_bar.xml 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
expand_button/src/main/res/values/attrs.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/assets/img/location_blue.svg
New file
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
    <!-- 外发光 (Outer Glow) -->
    <defs>
        <radialGradient id="outerGlow" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
            <stop offset="0%" style="stop-color:#803D8BFF; stop-opacity:0.5" />
            <stop offset="100%" style="stop-color:#003D8BFF; stop-opacity:0" />
        </radialGradient>
    </defs>
    <circle cx="80" cy="80" r="40" fill="url(#outerGlow)" />
    <!-- 内圆环 (Inner Circle Ring) -->
    <circle cx="80" cy="80" r="30" fill="white" stroke="#3D8BFF" stroke-width="4" />
</svg>
app/src/main/assets/js/map.js
@@ -143,7 +143,7 @@
    // 手机获取到定位后显示定位
    function locationOverLay(lng, lag) {
        console.log("function》》》》》locationOverLay");
//        console.log("function》》》》》locationOverLay");
        map.centerAndZoom(new T.LngLat(lng, lag), map.getZoom());
        let icon = new T.Icon({
            iconUrl: CONFIG.IMAGES.LOCATION,
@@ -161,7 +161,7 @@
    //设置地图中心点
    function setCenterAndZoom(lng, lat, thiszoom) {
        zoom = thiszoom;
        console.log("function》》》》》setCenterAndZoom>>>>lng:" + lng + ",lat:" + lat);
//        console.log("function》》》》》setCenterAndZoom>>>>lng:" + lng + ",lat:" + lat);
        map.centerAndZoom(new T.LngLat(lng, lat), zoom);
    }
@@ -180,10 +180,9 @@
            if (lastClickedMarker !== null) {
                lastClickedMarker.setIcon(createIcon(CONFIG.IMAGES.MARKER_BLUE));
            }
            if(lastClickedDivide!==null)
           {
           lastClickedDivide.setIcon(createIcon(CONFIG.IMAGES.DIVIDE_BLUE));
           }
            if (lastClickedDivide !== null) {
                lastClickedDivide.setIcon(createIcon(CONFIG.IMAGES.DIVIDE_BLUE));
            }
            if (isShowWaterIntakeDetail || isShowDivideDetail) {
                // 假如显示了取水口详情则隐藏取水口详情
                isShowWaterIntakeDetail = false;
@@ -232,13 +231,11 @@
    function isEqualsLngLat(data1, data2) {
        return data1.lat === data2.lat && data1.lng === data2.lng;
    }
    function addMarker(id, lng, lat, name) {
        addMarker(id, lng, lat, name, false)
    function addMarker(id, lng, lat, name, isShow) {
        addMyMarker(id, lng, lat, name, false, isShow)
    }
    //添加从原生传过来的坐标并显示在地图上
    function addMarker(id, lng, lat, name, isRed) {
        console.log("function》》》》》addMarker>>>id:" + id);
    function addMyMarker(id, lng, lat, name, isRed, isShow) {
        const iconUrl = isRed ? CONFIG.IMAGES.MARKER_RED : CONFIG.IMAGES.MARKER_BLUE;
        let marker = new T.Marker(
            new T.LngLat(lng, lat),
@@ -263,15 +260,16 @@
        if (isRed) {
            lastClickedMarker = marker;
        }
        // 将标记和标签存储到数组中
        waterIntakeMarkers.push({
            marker: marker,
            label: label
        });
        map.addOverLay(label);
        map.addOverLay(marker);
        if (isShow === "true" || isShow === true) {
            map.addOverLay(label);
            map.addOverLay(marker);
        }
        return "addMarker加载成功 id:" + id
    }
    //更新位坐标
@@ -311,96 +309,67 @@
    // 管网线路管理
    const PipelineManager = {
        lines: [],  // 存储所有完成的线路
        lines: [],
        currentLine: {
            points: [],  // 当前线路的点
            overlay: null  // 当前线路的图层对象
            points: [],
            overlay: null
        },
        // 线路样式配置
        style: {
            color: '#1890FF',
            weight: 3,
            opacity: 0.8
        },
        /**
         * 添加点到管网线路
         * @param {number} lng 经度
         * @param {number} lat 纬度
         * @param {boolean} isNewLine 是否开始新的线路
         */
        addPoint(lng, lat, isNewLine) {
            if (isNewLine) {
                this.finishCurrentLine();
            }
        addPoint(lng, lat, isNewLine, isShow) {
//            console.log(`Adding point: ${lng}, ${lat}, isNewLine: ${isNewLine}`); // 添加日志
            const point = new T.LngLat(lng, lat);
            this.currentLine.points.push(point);
            if (this.currentLine.points.length > 1) {
                this.updateCurrentLineDisplay();
            }
        },
        /**
         * 更新当前线路的显示
         */
        updateCurrentLineDisplay() {
            if (!this.currentLine.overlay) {
                // 创建新的线路图层
                this.currentLine.overlay = new T.Polyline(this.currentLine.points, this.style);
                map.addOverLay(this.currentLine.overlay);
            } else {
                // 更新现有线路的点
                this.currentLine.overlay.setLngLats(this.currentLine.points);
            }
        },
        /**
         * 完成当前线路
         */
        finishCurrentLine() {
            if (this.currentLine.points.length > 1) {
                if (this.currentLine.overlay) {
                    // 将当前线路添加到完成列表
                    this.lines.push(this.currentLine.overlay);
            // 修改判断逻辑,确保字符串"false"被正确处理
            const shouldCreateNewLine = isNewLine === true || isNewLine === "true" || !this.currentLine.overlay;
            if (shouldCreateNewLine) {
                // 创建新线路
                this.currentLine.points = [point];
                this.currentLine.overlay = new T.Polyline([point], this.style);
                if (isShow === true || isShow === "true") {
                    map.addOverLay(this.currentLine.overlay);
                }
            } else if (this.currentLine.overlay) {
                // 如果点数不足,清除图层
                map.removeOverLay(this.currentLine.overlay);
            } else {
                // 添加点到现有线路
                this.currentLine.points.push(point);
                this.currentLine.overlay.setLngLats(this.currentLine.points);
            }
            // 重置当前线路
            this.currentLine = {
                points: [],
                overlay: null
            };
            // 如果是新线路,将之前的线路保存到 lines 数组
            if (isNewLine === true || isNewLine === "true") {
                this.lines.push({
                    points: [...this.currentLine.points],
                    overlay: this.currentLine.overlay
                });
            }
        },
        /**
         * 显示所有线路
         */
        showAll() {
            this.lines.forEach(line => map.addOverLay(line));
            if (this.currentLine.overlay) {
                map.addOverLay(this.currentLine.overlay);
            }
//            console.log("showAllpipe" + this.lines.length);
            this.lines.forEach(line => {
                if (line.overlay) {
                    map.addOverLay(line.overlay);
                }
            });
        },
        /**
         * 隐藏所有线路
         */
        hideAll() {
            this.lines.forEach(line => map.removeOverLay(line));
            if (this.currentLine.overlay) {
                map.removeOverLay(this.currentLine.overlay);
            }
//            console.log("hideAllpipe" + this.lines.length);
            this.lines.forEach(line => {
                if (line.overlay) {
                    map.removeOverLay(line.overlay);
                }
            });
        },
        /**
         * 清除所有线路
         */
        clearAll() {
            this.hideAll();
            this.lines = [];
@@ -414,8 +383,8 @@
    /**
     * 添加管网线路点
     */
    function addPipeNetwork(lng, lat, isNewLine) {
        PipelineManager.addPoint(lng, lat, isNewLine);
    function addPipeNetwork(lng, lat, isNewLine, isShow) {
        PipelineManager.addPoint(lng, lat, isNewLine, isShow);
    }
    /**
@@ -491,14 +460,14 @@
    function refreshMarker(id, lng, lat, name) {
        map.removeOverLay(lastClickedMarker.label);
        map.removeOverLay(lastClickedMarker);
        addMarker(id, lng, lat, name, true);
        addMarker(id, lng, lat, name, true, true);
    }
    function mapMoveEnd(e) {
            const center = e.target.getCenter();
            window.Android.refreshCenter(center.getLng(), center.getLat());
        const center = e.target.getCenter();
        window.Android.refreshCenter(center.getLng(), center.getLat());
    }
@@ -524,11 +493,11 @@
        });
    }
    //添加分水房
    function addDivide(id, lng, lat, name) {
        addDivide(id, lng, lat, name, false)
    function addDivide(id, lng, lat, name, isShow) {
        addMyDivide(id, lng, lat, name, false, isShow)
    }
    //添加分水房
    function addDivide(id, lng, lat, name, isRed) {
    function addMyDivide(id, lng, lat, name, isRed, isShow) {
        console.log("function》》》》》addMarker>>>id:" + id);
        const iconUrl = isRed ? CONFIG.IMAGES.DIVIDE_RED : CONFIG.IMAGES.DIVIDE_BLUE;
@@ -561,9 +530,10 @@
            marker: marker,
            label: label
        });
        map.addOverLay(label);
        map.addOverLay(marker); // 将标注添加到地图中
        if (isShow === true || isShow === "true") {
            map.addOverLay(label);
            map.addOverLay(marker); // 将标注添加到地图中
        }
        return "addMarker加载成功 id:" + id
    }
    // 修改点击标注的图标
app/src/main/java/com/dayu/pipirrapp/bean/db/PipeNetWorkDataBean.java
New file
@@ -0,0 +1,34 @@
package com.dayu.pipirrapp.bean.db;
import java.util.List;
/**
 * PipNetWorkDataBean -
 *
 * @author zuoxiao
 * @version 1.0
 * @since 2025-02-11
 */
public class PipeNetWorkDataBean {
    List<PipeNetworkDataCoordinateBean> coordinates;
    String type;
    public List<PipeNetworkDataCoordinateBean> getCoordinates() {
        return coordinates;
    }
    public void setCoordinates(List<PipeNetworkDataCoordinateBean> coordinates) {
        this.coordinates = coordinates;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
}
app/src/main/java/com/dayu/pipirrapp/bean/db/PipeNetworkBean.java
@@ -2,8 +2,10 @@
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import androidx.room.TypeConverters;
import com.dayu.pipirrapp.bean.net.PipeNetworkResult;
import com.dayu.pipirrapp.db.converter.PipeNetworkConverter;
import java.util.List;
@@ -15,65 +17,17 @@
 * @since 2025-01-17
 */
@Entity
@TypeConverters(PipeNetworkConverter.class)
public class PipeNetworkBean {
    @PrimaryKey(autoGenerate = true)
    public long id;
    String type;
    String networkId;
    List<PipeNetworkResult.Data> data;
    @TypeConverters(PipeNetworkConverter.class)
    PipeNetWorkDataBean data;
    public class Data {
        public class Coordinate {
            String lat;
            String lng;
            public String getLat() {
                return lat;
            }
            public void setLat(String lat) {
                this.lat = lat;
            }
            public String getLng() {
                return lng;
            }
            public void setLng(String lng) {
                this.lng = lng;
            }
        }
        List<PipeNetworkResult.Data.Coordinate> coordinates;
        String type;
        String networkId;
        public List<PipeNetworkResult.Data.Coordinate> getCoordinates() {
            return coordinates;
        }
        public void setCoordinates(List<PipeNetworkResult.Data.Coordinate> coordinates) {
            this.coordinates = coordinates;
        }
        public String getType() {
            return type;
        }
        public void setType(String type) {
            this.type = type;
        }
        public String getNetworkId() {
            return networkId;
        }
        public void setNetworkId(String networkId) {
            this.networkId = networkId;
        }
    }
    public String getType() {
        return type;
@@ -91,11 +45,11 @@
        this.networkId = networkId;
    }
    public List<PipeNetworkResult.Data> getData() {
    public PipeNetWorkDataBean getData() {
        return data;
    }
    public void setData(List<PipeNetworkResult.Data> data) {
    public void setData(PipeNetWorkDataBean data) {
        this.data = data;
    }
app/src/main/java/com/dayu/pipirrapp/bean/db/PipeNetworkDataCoordinateBean.java
New file
@@ -0,0 +1,29 @@
package com.dayu.pipirrapp.bean.db;
/**
 * PipeNetworkDataCoordinateBean -
 *
 * @author zuoxiao
 * @version 1.0
 * @since 2025-02-11
 */
public class PipeNetworkDataCoordinateBean {
    String lat;
    String lng;
    public String getLat() {
        return lat;
    }
    public void setLat(String lat) {
        this.lat = lat;
    }
    public String getLng() {
        return lng;
    }
    public void setLng(String lng) {
        this.lng = lng;
    }
}
app/src/main/java/com/dayu/pipirrapp/bean/net/PipeNetworkResult.java
@@ -12,10 +12,12 @@
public class PipeNetworkResult {
    String type;
    String networkId;
    List<Data> data;
    Data data;
    public class Data {
    public static class Data {
        List<Coordinate> coordinates;
        String type;
        public class Coordinate {
            String lat;
            String lng;
@@ -37,9 +39,8 @@
            }
        }
        List<Coordinate> coordinates;
        String type;
        String networkId;
        public List<Coordinate> getCoordinates() {
            return coordinates;
@@ -57,13 +58,6 @@
            this.type = type;
        }
        public String getNetworkId() {
            return networkId;
        }
        public void setNetworkId(String networkId) {
            this.networkId = networkId;
        }
    }
    public String getType() {
@@ -82,11 +76,11 @@
        this.networkId = networkId;
    }
    public List<Data> getData() {
    public Data getData() {
        return data;
    }
    public void setData(List<Data> data) {
    public void setData(Data data) {
        this.data = data;
    }
}
app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java
@@ -32,4 +32,7 @@
    @Delete
    void delete(CenterPointBean adminData);
    @Query("DELETE FROM CenterPointBean")
    void deleteAll();
}
app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java
@@ -9,6 +9,7 @@
import com.dayu.pipirrapp.bean.db.DivideBean;
import com.dayu.pipirrapp.bean.db.MarkerBean;
import com.dayu.pipirrapp.bean.db.SearchResultBean;
import java.util.List;
@@ -54,4 +55,14 @@
    @Query("SELECT * FROM DivideBean")
    Maybe<List<DivideBean>> getAll();  // 改为返回Maybe<List<DivideBean>>
    @Query("SELECT * FROM DivideBean WHERE divideName LIKE '%' || :name || '%'")
    Single<List<DivideBean>> findByDivideNameLike(String name);
    @Query("SELECT divideName as name, 'divide' as type, lat, lng, address, divideId as id FROM DivideBean " +
           "WHERE divideName LIKE '%' || :keyword || '%' " +
           "UNION " +
           "SELECT name, 'marker' as type, lat, lng, address, id FROM MarkerBean " +
           "WHERE name LIKE '%' || :keyword || '%'")
    Single<List<SearchResultBean>> searchAllByKeyword(String keyword);
}
app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java
@@ -53,4 +53,6 @@
    @Query("SELECT * FROM MarkerBean")
    Maybe<List<MarkerBean>> getAll();
    @Query("SELECT * FROM MarkerBean WHERE name LIKE '%' || :name || '%'")
    Single<List<MarkerBean>> findByNameLike(String name);
}
app/src/main/java/com/dayu/pipirrapp/dao/PipeNetDao.java
@@ -1,5 +1,6 @@
package com.dayu.pipirrapp.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
@@ -17,6 +18,7 @@
/**
 * 管网相关dao
 */
@Dao
public interface PipeNetDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insert(PipeNetworkBean pipeNetworkBean);
app/src/main/java/com/dayu/pipirrapp/db/converter/PipeNetworkConverter.java
New file
@@ -0,0 +1,26 @@
package com.dayu.pipirrapp.db.converter;
import androidx.room.TypeConverter;
import com.dayu.pipirrapp.bean.db.PipeNetWorkDataBean;
import com.google.gson.Gson;
public class PipeNetworkConverter {
    @TypeConverter
    public static String fromPipeNetWorkDataBean(PipeNetWorkDataBean data) {
        if (data == null) {
            return null;
        }
        Gson gson = new Gson();
        return gson.toJson(data);
    }
    @TypeConverter
    public static PipeNetWorkDataBean toPipeNetWorkDataBean(String dataString) {
        if (dataString == null) {
            return null;
        }
        Gson gson = new Gson();
        return gson.fromJson(dataString, PipeNetWorkDataBean.class);
    }
}
app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java
@@ -32,7 +32,9 @@
import com.dayu.pipirrapp.bean.db.InspectionLocationBean;
import com.dayu.pipirrapp.bean.db.LatLonBean;
import com.dayu.pipirrapp.bean.db.MarkerBean;
import com.dayu.pipirrapp.bean.db.PipeNetWorkDataBean;
import com.dayu.pipirrapp.bean.db.PipeNetworkBean;
import com.dayu.pipirrapp.bean.db.PipeNetworkDataCoordinateBean;
import com.dayu.pipirrapp.bean.net.CenterPointResult;
import com.dayu.pipirrapp.bean.net.DivideListResult;
import com.dayu.pipirrapp.bean.net.DivideResult;
@@ -61,7 +63,9 @@
import com.dayu.pipirrapp.utils.ToastUtil;
import com.dayu.pipirrapp.utils.WebViewUtils;
import com.dayu.pipirrapp.view.ConfirmDialog;
import com.dayu.pipirrapp.view.SearchResultDialog;
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;
@@ -81,6 +85,8 @@
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Triple;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
/**
 * author: zuo
@@ -108,6 +114,7 @@
    //web加载时网页还没加载完时的数据
    List<MarkerBean> webNoFinishMarkerData = new ArrayList<>();
    List<DivideBean> webNoFinishDivideData = new ArrayList<>();
    List<PipeNetworkBean> webNoFinishPipeNetworkData = new ArrayList<>();
    //所有的Marker数据键为marker的Id
    Map<String, MarkerBean> markerBeanSet = new HashMap<>();
    //所有分水房
@@ -117,6 +124,9 @@
    public double centerLat;
    MarkerBean mMarkerBean;
    //是否显示Marker
    boolean isShowMarker, isShowPipeNetwork, isShowDivide;
    // 添加CompositeDisposable来管理所有订阅
    private CompositeDisposable compositeDisposable = new CompositeDisposable();
@@ -178,6 +188,8 @@
     * 异步加载本地数据
     */
    private void loadLocalData() {
        // 异步加载中心点数据
        compositeDisposable.add(
                DaoSingleton.getAsynchInstance(this.getContext()).centerPointDao().findFirst()
@@ -198,7 +210,7 @@
                            getCenterPoint();
                        })
        );
        isShowMarker = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.markerKeyIsShow, true);
        // 异步加载取水口数据
        compositeDisposable.add(
                DaoSingleton.getAsynchInstance(this.getContext()).markerDao().getAll()
@@ -221,7 +233,7 @@
                            getMarkerData();
                        })
        );
        isShowDivide = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.divideIsShow, true);
        // 异步加载分水房数据
        compositeDisposable.add(
                DaoSingleton.getAsynchInstance(this.getContext()).divideDao().getAll()
@@ -244,6 +256,8 @@
                            getDivideList();
                        })
        );
        isShowPipeNetwork = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.pipeNetworkIsShow, true);
        //异步加载管网数据
        compositeDisposable.add(
                DaoSingleton.getAsynchInstance(this.getContext()).pipeNetDao().getAll()
@@ -254,16 +268,15 @@
                                getPipeNetworkList();
                            } else {
                                for (PipeNetworkBean pipeNetworkBean : pipeNetworkBeans) {
                                    setMapPipe(pipeNetworkBean);
                                }
                            }
                        }, throwable -> {
                            Log.e(TAG, "Load divides error: " + throwable.getMessage());
                            getDivideList();
                            Log.e(TAG, "Load PipeNetworkBean error: " + throwable.getMessage());
                            getPipeNetworkList();
                        }, () -> {
                            // 当Maybe为空时调用
                            getDivideList();
                            getPipeNetworkList();
                        })
        );
    }
@@ -353,6 +366,12 @@
                setMapDivide(bean);
            }
        }
        if (!webNoFinishPipeNetworkData.isEmpty()) {
            for (PipeNetworkBean bean : webNoFinishPipeNetworkData) {
                setMapPipe(bean);
            }
        }
        //显示所有取水口
        MarkerUtils.showLocoMarks(MapFragment.this);
        //显示巡检状态并且显示因意外关闭的历史数据
@@ -391,7 +410,7 @@
                                    return markerBean;
                                })
                                .collect(Collectors.toList());
                        DaoSingleton.getInstance(MapFragment.this.getContext()).markerDao().deleteAll();
                        // 使用 CompositeDisposable 管理数据库插入操作
                        compositeDisposable.add(
                                DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).markerDao().insertAll(markerBeans)
@@ -424,7 +443,7 @@
        //巡检按钮
        binding.inspectButton.setOnClickListener(v -> {
            if (XXPermissions.isGranted(MapFragment.this.getContext(), Permission.ACCESS_BACKGROUND_LOCATION)) {
                new ConfirmDialog(MapFragment.this.getActivity(), (confirmDialog, v1) -> {
                new ConfirmDialog(MapFragment.this.getActivity(), "确认开始巡检吗?",(confirmDialog, v1) -> {
                    chageInspecState(InspectionUtils.STAT_INSPECTION_ONCLICK);
                    confirmDialog.dismiss();
                }).show();
@@ -456,28 +475,88 @@
            Intent issue = new Intent(MapFragment.this.getActivity(), AddIssueActivity.class);
            MapFragment.this.getActivity().startActivity(issue);
        });
        binding.expandButton.setLegendsArray(new Triple<>(
        binding.expandButton.setLegendsArray(new ExpandButton.Quadruple(
                        ContextCompat.getDrawable(requireContext(), R.drawable.marker_blue),
                        ContextCompat.getDrawable(requireContext(), R.drawable.marker_unselected),
                        "取水口"
                        "取水口", isShowMarker
                ),
                new Triple<>(
                new ExpandButton.Quadruple(
                        ContextCompat.getDrawable(requireContext(), R.drawable.divide_home_blue),
                        ContextCompat.getDrawable(requireContext(), R.drawable.divide_home_unselected),
                        "分水房"
                ));
                        "分水房", isShowDivide
                ),
                new ExpandButton.Quadruple(
                        ContextCompat.getDrawable(requireContext(), R.drawable.ic_pipenetwork_line),
                        ContextCompat.getDrawable(requireContext(), R.drawable.ic_pipenetwork_line_unselected),
                        "管网", isShowPipeNetwork
                )
        );
        binding.expandButton.setOnLegendItemClickListener((position, isSelected) -> {
            switch (position) {
                case 0:
                    showMarkers(isSelected);
                    SharedPreferencesHelper.getInstance(this.getContext()).put(CommonKeyName.markerKeyIsShow, isSelected);
                    break;
                case 1:
                    showDivideMarkers(isSelected);
                    SharedPreferencesHelper.getInstance(this.getContext()).put(CommonKeyName.divideIsShow, isSelected);
                    break;
                case 2:
                    showPipeLine(isSelected);
                    SharedPreferencesHelper.getInstance(this.getContext()).put(CommonKeyName.pipeNetworkIsShow, isSelected);
                    break;
            }
        });
        binding.searchButton.setOnSearchClickListener(s -> {
            if (s != null && !s.isEmpty()) {
                SearchResultDialog searchDialog = new SearchResultDialog(requireContext());
                searchDialog.setOnItemClickListener(item -> {
                    // 根据类型处理点击事件
//                    if ("marker".equals(item.getType())) {
//                        MarkerBean markerBean = new MarkerBean();
//                        markerBean.setId(item.getId());
//                        getInstakeDetail(markerBean);
//                    } else {
//                        DivideBean divideBean = new DivideBean();
//                        divideBean.setDivideId(item.getId());
//                        getDivideDetail(divideBean);
//                    }
                    // 跳转到选择的位置
                    mWebView.evaluateJavascript(
                        String.format("javascript:setCenterAndZoom(\"%s\",\"%s\",\"17\")",
                        item.getLng(),
                        item.getLat()),
                        null
                    );
                });
                // 执行搜索
                compositeDisposable.add(
                    DaoSingleton.getAsynchInstance(requireContext())
                        .divideDao()
                        .searchAllByKeyword(s)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(
                            results -> {
                                if (results != null && !results.isEmpty()) {
                                    searchDialog.setData(results);
                                    searchDialog.show();
                                } else {
                                    ToastUtil.showToast(requireContext(), "未找到相关结果");
                                }
                            },
                            throwable -> {
                                ToastUtil.showToast(requireContext(), "搜索失败:" + throwable.getMessage());
                            }
                        )
                );
            }
            return null;
        });
    }
    /**
     * 显示取水口详情
@@ -522,6 +601,7 @@
                    centerPointBean.setLat(t.getContent().getLat());
                    centerPointBean.setLng(t.getContent().getLng());
                    centerPointBean.setZoomMp(t.getContent().getZoomMp());
                    DaoSingleton.getInstance(MapFragment.this.getContext()).centerPointDao().deleteAll();
                    DaoSingleton.getInstance(MapFragment.this.getContext()).centerPointDao().insert(centerPointBean);
                    jumpCenterPoint();
                } else {
@@ -715,13 +795,13 @@
    }
    /**
     * 添加标注点
     * 添加取水口
     */
    public void setMapMarker(MarkerBean markerBean) {
        if (markerBean != null) {
            if (webViewIsFinished) {
                if (!TextUtils.isEmpty(markerBean.getLng()) && !TextUtils.isEmpty(markerBean.getLat())) {
                    mWebView.evaluateJavascript("javascript:addMarker(\"" + markerBean.getId() + "\",\"" + markerBean.getLng() + "\",\"" + markerBean.getLat() + "\",\"" + markerBean.getName() + "\")", new ValueCallback<String>() {
                    mWebView.evaluateJavascript("javascript:addMarker(\"" + markerBean.getId() + "\",\"" + markerBean.getLng() + "\",\"" + markerBean.getLat() + "\",\"" + markerBean.getName() + "\",\"" + isShowMarker + "\")", new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String value) {
                        }
@@ -737,13 +817,13 @@
    }
    /**
     * 添加取水口标注
     * 添加分水房
     */
    public void setMapDivide(DivideBean divide) {
        if (divide != null) {
            if (webViewIsFinished) {
                if (!TextUtils.isEmpty(divide.getLng()) && !TextUtils.isEmpty(divide.getLat())) {
                    mWebView.evaluateJavascript("javascript:addDivide(\"" + divide.getId() + "\",\"" + divide.getLng() + "\",\"" + divide.getLat() + "\",\"" + divide.getDivideName() + "\")", new ValueCallback<String>() {
                    mWebView.evaluateJavascript("javascript:addDivide(\"" + divide.getId() + "\",\"" + divide.getLng() + "\",\"" + divide.getLat() + "\",\"" + divide.getDivideName() + "\",\"" + isShowDivide + "\")", new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String value) {
                        }
@@ -761,24 +841,24 @@
    /**
     * 添加管网标注
     */
    public void setMapDivide(PipeNetworkBean pipeNetworkBean) {
//        if (divide != null) {
//            if (webViewIsFinished) {
//                for ()
//                if (!TextUtils.isEmpty(divide.getLng()) && !TextUtils.isEmpty(divide.getLat())) {
//                    mWebView.evaluateJavascript("javascript:addDivide(\"" + divide.getId() + "\",\"" + divide.getLng() + "\",\"" + divide.getLat() + "\",\"" + divide.getDivideName() + "\")", new ValueCallback<String>() {
//                        @Override
//                        public void onReceiveValue(String value) {
//                        }
//                    });
//                    divideBeanMap.put(divide.getId(), divide);
//                } else {
//                    MyLog.d("setMapDivide>" + divide.getDivideName() + "经纬度为空");
//                }
//            } else {
//                webNoFinishDivideData.add(divide);
//            }
//        }
    public void setMapPipe(PipeNetworkBean pipeNetworkBean) {
        if (pipeNetworkBean != null) {
            if (webViewIsFinished) {
                boolean isfrist = true;
                for (PipeNetworkDataCoordinateBean data : pipeNetworkBean.getData().getCoordinates()) {
                    if (!TextUtils.isEmpty(data.getLat()) && !TextUtils.isEmpty(data.getLng())) {
                        mWebView.evaluateJavascript("javascript:addPipeNetwork(\"" + data.getLng() + "\",\"" + data.getLat() + "\",\"" + isfrist +  "\",\"" + isShowPipeNetwork +"\")", new ValueCallback<String>() {
                            @Override
                            public void onReceiveValue(String value) {
                            }
                        });
                        isfrist = false;
                    }
                }
            } else {
                webNoFinishPipeNetworkData.add(pipeNetworkBean);
            }
        }
    }
    /**
@@ -1053,7 +1133,7 @@
                                setMapDivide(divideBean);
                                divideBeans.add(divideBean);
                            }
                            DaoSingleton.getInstance(MapFragment.this.getContext()).divideDao().deleteAll();
                            // 使用 CompositeDisposable 管理数据库插入操作
                            compositeDisposable.add(
                                    DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).divideDao().insertAll(divideBeans)
@@ -1094,23 +1174,40 @@
                        if (t.getContent() != null && t.getContent().size() > 0) {
                            List<PipeNetworkBean> pipeNetBeans = new ArrayList<>();
                            PipeNetworkBean pipeNetBean = new PipeNetworkBean();
                            for (PipeNetworkResult pipeNetworkResult : t.getContent()) {
                                PipeNetworkBean pipeNetBean = new PipeNetworkBean();
                                pipeNetBean.setNetworkId(pipeNetworkResult.getNetworkId());
                                pipeNetBean.setType(pipeNetworkResult.getType());
                                pipeNetBean.setData(pipeNetworkResult.getData());
                                PipeNetWorkDataBean data = new PipeNetWorkDataBean();
                                data.setType(pipeNetworkResult.getData().getType());
                                List<PipeNetworkDataCoordinateBean> coordinates = new ArrayList<>();
                                for (PipeNetworkResult.Data.Coordinate dataBean : pipeNetworkResult.getData().getCoordinates()) {
                                    PipeNetworkDataCoordinateBean coordinate = new PipeNetworkDataCoordinateBean();
                                    coordinate.setLat(dataBean.getLat());
                                    coordinate.setLng(dataBean.getLng());
                                    coordinates.add(coordinate);
                                }
                                data.setCoordinates(coordinates);
                                pipeNetBean.setData(data);
                                setMapPipe(pipeNetBean);
                                pipeNetBeans.add(pipeNetBean);
                            }
                            DaoSingleton.getInstance(MapFragment.this.getContext()).pipeNetDao().deleteAll();
                            // 使用 RxJava 异步插入数据
                            DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).pipeNetDao().insertAll(pipeNetBeans)
                                    .subscribeOn(Schedulers.io()) // 在 IO 线程上执行
                                    .observeOn(AndroidSchedulers.mainThread()) // 在主线程上观察
                                    .subscribe(() -> {
                                        // 插入成功
                                        Log.i("mWebView", "数据插入成功");
                                    }, throwable -> {
                                        // 插入失败
                                        Log.e("mWebView", "数据插入失败: " + throwable.getMessage());
                                    });
                            compositeDisposable.add(
                                    DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).pipeNetDao().insertAll(pipeNetBeans)
                                            .subscribeOn(Schedulers.io()) // 在 IO 线程上执行
                                            .observeOn(AndroidSchedulers.mainThread()) // 在主线程上观察
                                            .subscribe(() -> {
                                                // 插入成功
                                                Log.i("mWebView", "数据插入成功");
                                            }, throwable -> {
                                                // 插入失败
                                                Log.e("mWebView", "数据插入失败: " + throwable.getMessage());
                                            })
                            );
                        }
@@ -1233,4 +1330,14 @@
        }
    }
    private void showPipeLine(boolean isSelected) {
        if (isSelected) {
            mWebView.evaluateJavascript("javascript:showAllPipeLines()", value -> {
            });
        } else {
            mWebView.evaluateJavascript("javascript:hideAllPipeLines()", value -> {
            });
        }
    }
}
app/src/main/java/com/dayu/pipirrapp/service/MyCommonService.java
File was deleted
app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java
@@ -24,13 +24,20 @@
    public final static String NetworkCallback = "NetworkCallback";
    //刷新新工单小红点
    public final static String RedLotRefresh="RedLotRefresh";
    public final static String RedLotRefresh = "RedLotRefresh";
    //创建通知
    public final static String CreateNotification="CreateNotification";
    public final static String CreateNotification = "CreateNotification";
    /**
     * 刷新数据事件
     */
    public static final String refreshData = "refreshData";
    //取水口是否显示的key
    public static final String markerKeyIsShow = "markerKeyIsShow";
    public static final String divideIsShow = "divideIsShow";
    public static final String pipeNetworkIsShow = "pipeNetworkIsShow";
}
app/src/main/java/com/dayu/pipirrapp/view/SearchResultDialog.java
New file
@@ -0,0 +1,129 @@
package com.dayu.pipirrapp.view;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.dayu.pipirrapp.R;
import com.dayu.pipirrapp.bean.db.SearchResultBean;
import java.util.ArrayList;
import java.util.List;
public class SearchResultDialog extends Dialog {
    private Context context;
    private List<SearchResultBean> searchResults = new ArrayList<>();
    private OnItemClickListener onItemClickListener;
    public interface OnItemClickListener {
        void onItemClick(SearchResultBean item);
    }
    public SearchResultDialog(@NonNull Context context) {
        super(context, R.style.CustomDialog);
        this.context = context;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.dialog_search_result);
        // 设置对话框位置和大小
        Window window = getWindow();
        if (window != null) {
            WindowManager.LayoutParams params = window.getAttributes();
            params.gravity = Gravity.TOP;
            params.width = WindowManager.LayoutParams.MATCH_PARENT;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            window.setAttributes(params);
        }
        ListView listView = findViewById(R.id.listView);
        SearchResultAdapter adapter = new SearchResultAdapter();
        listView.setAdapter(adapter);
        listView.setOnItemClickListener((parent, view, position, id) -> {
            if (onItemClickListener != null) {
                onItemClickListener.onItemClick(searchResults.get(position));
            }
            dismiss();
        });
    }
    public void setData(List<SearchResultBean> results) {
        this.searchResults.clear();
        this.searchResults.addAll(results);
        if (isShowing()) {
            ListView listView = findViewById(R.id.listView);
            ((SearchResultAdapter) listView.getAdapter()).notifyDataSetChanged();
        }
    }
    public void setOnItemClickListener(OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }
    private class SearchResultAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return searchResults.size();
        }
        @Override
        public SearchResultBean getItem(int position) {
            return searchResults.get(position);
        }
        @Override
        public long getItemId(int position) {
            return position;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
            if (convertView == null) {
                convertView = LayoutInflater.from(context).inflate(R.layout.item_search_result, parent, false);
                holder = new ViewHolder();
                holder.nameText = convertView.findViewById(R.id.nameText);
                holder.addressText = convertView.findViewById(R.id.addressText);
                holder.typeIcon = convertView.findViewById(R.id.typeIcon);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            SearchResultBean item = getItem(position);
            holder.nameText.setText(item.getName());
            holder.addressText.setText(item.getAddress());
            // 根据类型设置不同的图标
            if ("marker".equals(item.getType())) {
                holder.typeIcon.setImageResource(R.drawable.marker_blue);
            } else {
                holder.typeIcon.setImageResource(R.drawable.divide_home_blue);
            }
            return convertView;
        }
        class ViewHolder {
            TextView nameText;
            TextView addressText;
            ImageView typeIcon;
        }
    }
}
app/src/main/res/drawable/ic_pipenetwork_line.xml
New file
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="2dp"
    android:viewportWidth="24"
    android:viewportHeight="2">
    <path
        android:pathData="M0,1L24,1"
        android:strokeWidth="2"
        android:strokeColor="#1890FF"
        android:strokeLineCap="round"/>
</vector>
app/src/main/res/drawable/ic_pipenetwork_line_unselected.xml
New file
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="2dp"
    android:viewportWidth="24"
    android:viewportHeight="2">
    <path
        android:pathData="M0,1L24,1"
        android:strokeWidth="2"
        android:strokeColor="#757575"
        android:strokeLineCap="round"/>
</vector>
app/src/main/res/drawable/location_marker_bg.xml
New file
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 外发光 -->
    <item>
        <shape android:shape="oval">
            <gradient
                android:type="radial"
                android:gradientRadius="40dp"
                android:startColor="#803D8BFF"
                android:endColor="#003D8BFF"/>
            <size android:width="80dp" android:height="80dp"/>
        </shape>
    </item>
    <!-- 内圆环 -->
    <item
        android:left="20dp"
        android:top="20dp"
        android:right="20dp"
        android:bottom="20dp">
        <shape android:shape="oval">
            <solid android:color="#FFFFFF"/>
            <stroke
                android:width="4dp"
                android:color="#3D8BFF"/>
        </shape>
    </item>
    <!-- 动画图标将在代码中设置为该图层的前景 -->
</layer-list>
app/src/main/res/drawable/walking_worker_animation.xml
New file
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/worker_frame1"
        android:duration="500" />
    <item
        android:drawable="@drawable/worker_frame2"
        android:duration="500" />
</animation-list>
app/src/main/res/drawable/worker_frame1.xml
New file
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="48dp"
    android:height="48dp"
    android:viewportWidth="48"
    android:viewportHeight="48">
    <path
        android:fillColor="#3D8BFF"
        android:pathData="M24,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
    <path
        android:fillColor="#3D8BFF"
        android:pathData="M32,28h-2v-6c0,-3.31 -2.69,-6 -6,-6s-6,2.69 -6,6v6h-2v-6c0,-4.42 3.58,-8 8,-8s8,3.58 8,8V28z"/>
    <path
        android:fillColor="#3D8BFF"
        android:pathData="M22,30l-4,12h2l4,-10l4,10h2l-4,-12H22z"/>
</vector>
app/src/main/res/drawable/worker_frame2.xml
New file
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="48dp"
    android:height="48dp"
    android:viewportWidth="48"
    android:viewportHeight="48">
    <path
        android:fillColor="#3D8BFF"
        android:pathData="M24,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
    <path
        android:fillColor="#3D8BFF"
        android:pathData="M32,28h-2v-6c0,-3.31 -2.69,-6 -6,-6s-6,2.69 -6,6v6h-2v-6c0,-4.42 3.58,-8 8,-8s8,3.58 8,8V28z"/>
    <path
        android:fillColor="#3D8BFF"
        android:pathData="M20,30l-2,12h2l2,-10l6,10h2l-6,-12H20z"/>
</vector>
app/src/main/res/layout/dialog_search_result.xml
New file
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="50dp"
    android:layout_marginBottom="50dp"
    android:layout_marginLeft="20dp"
    android:layout_marginRight="20dp"
    android:background="@android:color/white"
    android:orientation="vertical">
    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:divider="@android:color/darker_gray"
        android:dividerHeight="0.5dp"
        android:maxHeight="300dp" />
</LinearLayout>
app/src/main/res/layout/fragment_map.xml
@@ -79,15 +79,31 @@
    </RelativeLayout>
    <com.example.expand_button.CustomSearchBar
        android:id="@+id/searchButton"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/inspectRL"
        android:layout_marginTop="20dp"
        android:layout_marginRight="15dp"
        android:background="@drawable/ic_blue_background_down"
        android:textColor="@color/white"
        android:textSize="18sp" />
    <TextView
        android:id="@+id/inspectButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_height="40dp"
        android:minWidth="40dp"
        android:layout_below="@+id/searchButton"
        android:layout_alignParentRight="true"
        android:layout_marginTop="60dp"
        android:layout_marginTop="10dp"
        android:layout_marginRight="15dp"
        android:background="@drawable/ic_blue_background"
        android:padding="10dp"
     android:gravity="center"
        android:text="巡"
        android:textColor="@color/white"
        android:textSize="18sp" />
@@ -96,13 +112,15 @@
    <TextView
        android:id="@+id/putButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_height="40dp"
        android:layout_alignParentRight="true"
        android:layout_marginTop="120dp"
        android:layout_marginTop="10dp"
        android:layout_below="@+id/inspectButton"
        android:layout_marginRight="15dp"
        android:background="@drawable/ic_green_bg"
        android:padding="10dp"
        android:text="报"
        android:minWidth="40dp"
        android:gravity="center"
        android:textColor="@color/white"
        android:textSize="18sp" />
@@ -111,10 +129,11 @@
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginTop="180dp"
        android:layout_marginTop="10dp"
        android:layout_marginRight="15dp"
        android:layout_marginLeft="15dp"
        android:layout_below="@+id/putButton"
        android:background="@drawable/ic_green_bg"
        android:padding="10dp"
        android:textColor="@color/white"
        android:textSize="18sp"
        app:animDuration="300"
app/src/main/res/layout/item_search_result.xml
New file
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="12dp">
    <ImageView
        android:id="@+id/typeIcon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_gravity="center_vertical" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:orientation="vertical">
        <TextView
            android:id="@+id/nameText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#333333"
            android:textSize="16sp" />
        <TextView
            android:id="@+id/addressText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:textColor="#666666"
            android:textSize="14sp" />
    </LinearLayout>
</LinearLayout>
app/src/main/res/values/styles.xml
@@ -13,6 +13,16 @@
        <item name="android:windowSoftInputMode">adjustPan</item>
    </style>
    <!-- 自定义对话框样式 -->
    <style name="CustomDialog" parent="Theme.AppCompat.Light.Dialog">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
        <item name="android:backgroundDimEnabled">true</item>
    </style>
expand_button/src/main/java/com/example/expand_button/CustomSearchBar.kt
New file
@@ -0,0 +1,178 @@
package com.example.expand_button
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
class CustomSearchBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
    private var isExpanded = false
    private var searchIcon: ImageView
    private var searchInput: EditText
    private var searchButton: TextView
    private var onSearchClickListener: ((String) -> Unit)? = null
    private var expandedWidth = 0
    private var animationDuration: Long = 300
    private var initialWidth = 0
    init {
        // 创建一个新的LayoutInflater,避免使用带背景的布局
        val inflater = LayoutInflater.from(context).cloneInContext(context)
        val root = inflater.inflate(R.layout.layout_search_bar, this, false)
        // 清除布局文件中的背景
        root.background = null
        // 添加布局
        addView(root)
        // 初始化视图
        searchIcon = findViewById(R.id.iv_search)
        searchInput = findViewById(R.id.et_search)
        searchButton = findViewById(R.id.tv_search)
        // 设置初始状态
        post {
            // 保存初始宽度
            initialWidth = width
            // 确保搜索图标居中显示
            (searchIcon.layoutParams as RelativeLayout.LayoutParams).apply {
                removeRule(ALIGN_PARENT_START)
                addRule(CENTER_IN_PARENT)
                setMargins(0, 0, 0, 0)
            }
            // 初始状态隐藏输入框和搜索按钮
            searchInput.visibility = View.GONE
            searchButton.visibility = View.GONE
        }
        // 设置点击监听
        searchIcon.setOnClickListener {
            if (!isExpanded) {
                expand()
            }
        }
        searchButton.setOnClickListener {
            onSearchClickListener?.invoke(searchInput.text.toString())
            collapse()
        }
        // 设置整个布局的点击监听
        setOnClickListener {
            if (!isExpanded) {
                expand()
            }
        }
    }
    private fun expand() {
        if (isExpanded) return
        // 计算展开后的宽度(屏幕宽度减去左右边距)
        if (expandedWidth == 0) {
            expandedWidth = context.resources.displayMetrics.widthPixels -
                    (32 * context.resources.displayMetrics.density).toInt()
        }
        // 创建宽度动画
        ValueAnimator.ofInt(initialWidth, expandedWidth).apply {
            duration = animationDuration
            addUpdateListener { animator ->
                layoutParams = layoutParams.apply {
                    width = animator.animatedValue as Int
                }
            }
            start()
        }
        // 重置搜索图标位置
        (searchIcon.layoutParams as RelativeLayout.LayoutParams).apply {
            removeRule(CENTER_VERTICAL)
            addRule(ALIGN_PARENT_START)
            setMargins(
                (8 * context.resources.displayMetrics.density).toInt(),
                0,
                0,
                0
            )
        }
        // 显示输入框和搜索按钮
        searchInput.visibility = View.VISIBLE
        searchButton.visibility = View.VISIBLE
        searchInput.requestFocus()
        isExpanded = true
    }
    private fun collapse() {
        if (!isExpanded) return
        // 创建宽度动画
        ValueAnimator.ofInt(expandedWidth, initialWidth).apply {
            duration = animationDuration
            addUpdateListener { animator ->
                layoutParams = layoutParams.apply {
                    width = animator.animatedValue as Int
                }
            }
            start()
        }
        // 重置搜索图标到中心位置
        (searchIcon.layoutParams as RelativeLayout.LayoutParams).apply {
            removeRule(ALIGN_PARENT_START)
            addRule(CENTER_IN_PARENT)
            setMargins(0, 0, 0, 0)
        }
        // 隐藏输入框和搜索按钮
        searchInput.visibility = View.GONE
        searchButton.visibility = View.GONE
        searchInput.setText("")
        isExpanded = false
    }
    /**
     * 设置搜索按钮点击监听器
     * @param listener 回调函数,参数为输入框中的文本内容
     */
    fun setOnSearchClickListener(listener: (String) -> Unit) {
        onSearchClickListener = listener
    }
    /**
     * 设置动画时长
     */
    fun setAnimationDuration(duration: Long) {
        animationDuration = duration
    }
    /**
     * 设置提示文字
     */
    fun setHint(hint: String) {
        searchInput.hint = hint
    }
    /**
     * 获取输入的文本
     */
    fun getText(): String {
        return searchInput.text.toString()
    }
}
expand_button/src/main/java/com/example/expand_button/ExpandButton.kt
@@ -499,49 +499,20 @@
    fun setButtonId(id: String) {
        this.buttonId = id
        // 加载保存的状态
        loadStates()
    }
    /**
     * 保存所有图例项的状态
     */
    private fun saveStates() {
        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
        val states = legendItems.map { it.isSelected }
        prefs.edit().putString("${KEY_LEGEND_STATES}_$buttonId", states.joinToString(",")).apply()
    }
    /**
     * 加载保存的状态
     */
    private fun loadStates() {
        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
        val savedStates = prefs.getString("${KEY_LEGEND_STATES}_$buttonId", null)
        if (savedStates != null && legendItems.isNotEmpty()) {
            val states = savedStates.split(",").map { it.toBoolean() }
            states.forEachIndexed { index, state ->
                if (index < legendItems.size) {
                    legendItems[index].isSelected = state
                }
            }
            invalidate()
        }
    }
    /**
     * 设置图例内容
     */
    @JvmName("setLegendsList")
    fun setLegends(items: List<Triple<Drawable, Drawable, String>>) {
        legendItems = items.map { (selectedIcon, unselectedIcon, description) ->
    fun setLegends(items: List<Quadruple<Drawable, Drawable, String, Boolean>>) {
        legendItems = items.map { (selectedIcon, unselectedIcon, description, isSelected) ->
            selectedIcon.setBounds(0, 0, iconSize, iconSize)
            unselectedIcon.setBounds(0, 0, iconSize, iconSize)
            LegendItem(selectedIcon, unselectedIcon, description)
            LegendItem(selectedIcon, unselectedIcon, description, isSelected)
        }
        // 加载保存的状态
        loadStates()
        
        if (!isExpanded) {
            text = collapsedText
@@ -553,8 +524,21 @@
    // 添加一个 Java 友好的方法
    @JvmName("setLegendsArray")
    fun setLegends(vararg items: Triple<Drawable, Drawable, String>) {
    fun setLegends(vararg items: Quadruple<Drawable, Drawable, String, Boolean>) {
        setLegends(items.toList())
    }
    // 添加一个数据类来表示四元组
    data class Quadruple<A, B, C, D>(
        val first: A,
        val second: B,
        val third: C,
        val fourth: D
    )
    // 添加一个便捷的扩展函数来创建 Quadruple
    fun <A, B, C, D> quadrupleOf(first: A, second: B, third: C, fourth: D): Quadruple<A, B, C, D> {
        return Quadruple(first, second, third, fourth)
    }
    /**
@@ -635,10 +619,7 @@
            index, 
            legendItems[index].isSelected
        )
        // 保存状态
        saveStates()
        invalidate()
    }
expand_button/src/main/java/com/example/expand_button/ExpandSearchView.kt
New file
@@ -0,0 +1,449 @@
package com.example.expand_button
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.text.InputType
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.content.ContextCompat
import android.util.TypedValue
import androidx.core.animation.addListener
import android.util.Log
class ExpandSearchView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatEditText(context, attrs, defStyleAttr) {
    // 搜索图标
    private val searchIcon: Drawable = ContextCompat.getDrawable(
        context,
        R.drawable.ic_search
    )!!.mutate()
    // 搜索按钮文字
    private var searchButtonText: String = "搜索"
    // 搜索按钮点击监听器
    private var onSearchClickListener: (() -> Unit)? = null
    // 当前是否处于展开状态
    private var isExpanded: Boolean = false
    // 动画持续时间,默认300毫秒
    private var animationDuration: Long = 300
    // 搜索图标与文字的间距,默认为8dp
    private var iconMargin: Float = 8 * context.resources.displayMetrics.density
    // 搜索按钮与输入框的间距,默认为8dp
    private var searchButtonMargin: Float = 8 * context.resources.displayMetrics.density
    // 提示文字
    private var hintText: String = "请输入取水口或分水房名称"
    // 将defaultHeight移到类的属性中
    private val defaultHeight = (40 * context.resources.displayMetrics.density).toInt()
    // 添加展开方向的属性
    private var expandDirection: Int = EXPAND_LEFT
    companion object {
        const val EXPAND_LEFT = 0
        const val EXPAND_RIGHT = 1
    }
    init {
        // 设置单行输入
        maxLines = 1
        isSingleLine = true
        // 设置搜索图标的大小
        val iconSize = (24 * context.resources.displayMetrics.density).toInt()
        // 不在这里设置图标边界,而是在onDraw中设置
        // 设置提示文字
        hint = hintText
        // 设置文字垂直居中
        gravity = Gravity.CENTER_VERTICAL or Gravity.START
        // 设置固定高度
        layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            defaultHeight
        )
        // 初始状态下禁用输入
        inputType = InputType.TYPE_NULL
        // 初始状态下隐藏输入框
        if (!isExpanded) {
            hint = ""
            setText("")
            isEnabled = false
            // 确保初始宽度为收起状态的宽度
            post {
                layoutParams = layoutParams?.apply {
                    width = defaultHeight // 使用相同的高度作为宽度,保持正方形
                    height = defaultHeight // 保持固定高度
                }
                invalidate() // 添加这行确保图标重绘
            }
        }
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 设置固定高度
        val defaultHeight = (40 * context.resources.displayMetrics.density).toInt()
        if (!isExpanded) {
            // 收缩状态下的宽度设为正方形
            setMeasuredDimension(defaultHeight, defaultHeight)
        } else {
            // 展开状态下保持指定的宽度,但高度固定
            setMeasuredDimension(measuredWidth, defaultHeight)
        }
    }
    override fun onDraw(canvas: Canvas) {
        if (!isExpanded) {
            // 计算图标位置并绘制
            val centerX = width / 2
            val centerY = height / 2
            val iconSize = (24 * context.resources.displayMetrics.density).toInt()
            val iconHalfWidth = iconSize / 2
            val iconHalfHeight = iconSize / 2
            searchIcon.setBounds(
                centerX - iconHalfWidth,
                centerY - iconHalfHeight,
                centerX + iconHalfWidth,
                centerY + iconHalfHeight
            )
            searchIcon.draw(canvas)
        } else {
            val iconSize = searchIcon.intrinsicWidth
            val centerY = height / 2
            val iconLeftMargin = (5 * context.resources.displayMetrics.density).toInt()
            // 绘制图标
            searchIcon.setBounds(
                iconLeftMargin,
                centerY - iconSize / 2,
                iconLeftMargin + iconSize,
                centerY + iconSize / 2
            )
            searchIcon.draw(canvas)
            // 绘制分隔线
            paint.apply {
                color = 0xFFFFFFFF.toInt()
                strokeWidth = (1 * context.resources.displayMetrics.density)
            }
            val dividerX = iconLeftMargin + iconSize + (iconMargin / 2)
            canvas.drawLine(
                dividerX,
                height * 0.2f,
                dividerX,
                height * 0.8f,
                paint
            )
            // 记录文本绘制区域的信息
            val buttonWidth = paint.measureText(searchButtonText)
            val textLeft = compoundPaddingLeft.toFloat()  // 使用复合内边距
            val textRight = width - compoundPaddingRight.toFloat()  // 使用复合内边距
            // 只在实际绘制文本时记录一次区域信息
            if (text!!.isNotEmpty() && !text.toString().endsWith("\n")) {
                Log.d("ExpandSearchView", "文本绘制区域信息: 控件尺寸[宽度:$width, 高度:$height], " +
                    "文本区域[左边界:$textLeft, 右边界:$textRight, 可用宽度:${textRight - textLeft}], " +
                    "内边距[左:$paddingLeft, 右:$paddingRight, 复合左:$compoundPaddingLeft, 复合右:$compoundPaddingRight], " +
                    "文本信息[当前文本:'${text}', 长度:${text?.length}, 光标位置:$selectionStart], " +
                    "绘制参数[图标大小:$iconSize, 图标左边距:$iconLeftMargin, 按钮宽度:$buttonWidth, 按钮边距:$searchButtonMargin], " +
                    "位置信息[translationX:$translationX, scrollX:$scrollX, 文本偏移:${textLeft - compoundPaddingLeft}]")
            }
            // 画文本
            super.onDraw(canvas)
            // 绘制按钮文本
            paint.apply {
                color = currentTextColor
                textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16f, context.resources.displayMetrics)
                textAlign = Paint.Align.CENTER
            }
            val buttonText = searchButtonText
            val fontMetrics = paint.fontMetrics
            val textY = height / 2f + (fontMetrics.bottom - fontMetrics.top) / 2f - fontMetrics.bottom
            val buttonX = width - buttonWidth / 2 - searchButtonMargin
            // 记录搜索按钮位置信息
            Log.d("ExpandSearchView", "搜索按钮信息: 位置[X:$buttonX, Y:$textY], " +
                "区域[左:${buttonX - buttonWidth/2}, 右:${buttonX + buttonWidth/2}, 上:0, 下:$height], " +
                "尺寸[宽度:$buttonWidth, 高度:$height]")
            canvas.drawText(
                buttonText,
                buttonX,
                textY,
                paint
            )
        }
    }
    override fun onTextChanged(
        text: CharSequence?,
        start: Int,
        lengthBefore: Int,
        lengthAfter: Int
    ) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter)
        // 只在展开状态且实际有新字符输入时才输出日志
        if (isExpanded && lengthAfter > lengthBefore) {
            val newChar = text?.subSequence(start, start + lengthAfter - lengthBefore)
            Log.d("ExpandSearchView", "新字符输入信息: 输入内容[新字符:'$newChar', 完整文本:'$text'], " +
                "位置信息[输入位置:$start, 光标位置:$selectionStart], " +
                "文本区域[左内边距:$paddingLeft, 右内边距:$paddingRight, 可见宽度:${width - paddingLeft - paddingRight}], " +
                "滚动状态[scrollX:$scrollX, translationX:$translationX]")
        }
    }
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_UP) {
            if (isExpanded) {
                // 展开状态下,检查是否点击了搜索按钮
                val buttonText = searchButtonText
                val buttonWidth = paint.measureText(buttonText)
                val buttonArea = RectF(
                    width - buttonWidth - searchButtonMargin * 2,
                    0f,
                    width.toFloat(),
                    height.toFloat()
                )
                if (buttonArea.contains(event.x, event.y)) {
                    onSearchClickListener?.invoke()
                    post { toggleExpand() }
                    return true
                }
            } else {
                toggleExpand()
                return true
            }
        }
        return super.onTouchEvent(event)
    }
    override fun performClick(): Boolean {
        // 确保可访问性服务正常工作
        super.performClick()
        return true
    }
    /**
     * 切换展开/收起状态
     */
    private fun toggleExpand() {
        isExpanded = !isExpanded
        val startWidth = width
        val screenWidth = context.resources.displayMetrics.widthPixels
        val location = IntArray(2)
        getLocationInWindow(location)
        // 获取父布局信息
        val parent = parent as? ViewGroup
        val parentWidth = parent?.width ?: screenWidth
        // 获取或设置边距
        var params = layoutParams as? ViewGroup.MarginLayoutParams
        if (params == null) {
            params = ViewGroup.MarginLayoutParams(layoutParams)
            layoutParams = params
        }
        val margin = params.rightMargin.takeIf { it > 0 }
            ?: (45 * context.resources.displayMetrics.density).toInt() // 默认45dp的边距
        // 计算目标宽度,考虑两边的边距
        val endWidth = if (isExpanded) {
            val iconSize = searchIcon.intrinsicWidth
            val iconMarginPx = (5 * context.resources.displayMetrics.density).toInt()
            val buttonWidth = paint.measureText(searchButtonText).toInt()
            val hintWidth = paint.measureText(hintText)
            val minRequiredWidth = iconMarginPx + iconSize + iconMargin.toInt() + // 左侧图标区域
                hintWidth.toInt() + // 提示文字宽度
                buttonWidth + (searchButtonMargin * 2).toInt() + // 按钮区域
                (16 * context.resources.displayMetrics.density).toInt() // 额外边距
            maxOf(minRequiredWidth, parentWidth - (margin * 2)) // 取所需宽度和父容器宽度的较大值
        } else {
            defaultHeight
        }
        val initialTranslationX = translationX
        ValueAnimator.ofFloat(0f, 1f).apply {
            duration = animationDuration
            addUpdateListener { animator ->
                val fraction = animator.animatedValue as Float
                val currentWidth = if (isExpanded) {
                    startWidth + ((endWidth - startWidth) * fraction).toInt()
                } else {
                    startWidth - ((startWidth - endWidth) * fraction).toInt()
                }
                (layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
                    width = currentWidth
                    rightMargin = margin
                    leftMargin = if (isExpanded) margin else 0 // 展开时添加左边距
                }?.also { params ->
                    layoutParams = params
                }
                // 修正位移,当控件展开时translationX设置为0
                if (isExpanded) {
                    translationX = 0f
                } else {
                    translationX = initialTranslationX * (1 - fraction)
                }
                requestLayout()
                invalidate()
            }
            addListener(onStart = {
                if (!isExpanded) {
                    setText("")
                }
            }, onEnd = {
                if (!isExpanded) {
                    translationX = 0f
                    setPadding(
                        defaultHeight / 4,
                        defaultHeight / 4,
                        defaultHeight / 4,
                        defaultHeight / 4
                    )
                    hint = ""
                    isEnabled = false
                    clearFocus()
                    inputType = InputType.TYPE_NULL
                    isFocusable = false
                    isFocusableInTouchMode = false
                    // 收起时移除左边距
                    (layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
                        leftMargin = 0
                    }?.also { params ->
                        layoutParams = params
                    }
                }
                if (isExpanded) {
                    hint = hintText
                    isEnabled = true
                    inputType = InputType.TYPE_CLASS_TEXT
                    isFocusable = true
                    isFocusableInTouchMode = true
                    val iconSize = searchIcon.intrinsicWidth
                    val iconMarginPx = (5 * context.resources.displayMetrics.density).toInt()
                    val verticalPadding = (8 * context.resources.displayMetrics.density).toInt()
                    val buttonWidth = paint.measureText(searchButtonText).toInt()
                    val hintWidth = paint.measureText(hintText)
                    // 记录提示文字信息
                    Log.d("ExpandSearchView", "提示文字信息: 文本['$hintText'], " +
                        "宽度信息[文本宽度:$hintWidth, 可用宽度:${width - iconMarginPx - iconSize - iconMargin - buttonWidth - searchButtonMargin * 2}], " +
                        "内边距[左:${iconMarginPx + iconSize + iconMargin.toInt()}, 右:${buttonWidth + (searchButtonMargin * 2).toInt()}]")
                    setPadding(
                        iconMarginPx + iconSize + iconMargin.toInt(), // 左边距包含图标和间距
                        verticalPadding,
                        buttonWidth + (searchButtonMargin * 2).toInt(), // 右边距包含按钮和间距
                        verticalPadding
                    )
                    requestFocus()
                }
            })
            start()
        }
    }
    /**
     * 设置搜索图标
     */
    fun setSearchIcon(icon: Drawable) {
        val size = (24 * context.resources.displayMetrics.density).toInt()
        icon.setBounds(0, 0, size, size)
        searchIcon.bounds = icon.bounds
        invalidate()
    }
    /**
     * 设置搜索图标与文字的间距
     */
    fun setIconMargin(margin: Float) {
        iconMargin = margin
        invalidate()
    }
    /**
     * 设置提示文字
     */
    fun setHintText(text: String) {
        hintText = text
        if (isExpanded) {
            hint = hintText
        }
    }
    /**
     * 设置动画时长
     */
    fun setAnimationDuration(duration: Long) {
        animationDuration = duration
    }
    /**
     * 设置搜索按钮与输入框的间距
     */
    fun setSearchButtonMargin(margin: Float) {
        searchButtonMargin = margin
        invalidate()
    }
    /**
     * 设置搜索按钮点击监听器
     */
    fun setOnSearchClickListener(listener: () -> Unit) {
        onSearchClickListener = listener
    }
    /**
     * 设置搜索按钮文字
     */
    fun setSearchButtonText(text: String) {
        searchButtonText = text
        if (isExpanded) {
            invalidate()
        }
    }
    /**
     * 设置展开方向
     */
    fun setExpandDirection(direction: Int) {
        require(direction == EXPAND_LEFT || direction == EXPAND_RIGHT) {
            "Direction must be either EXPAND_LEFT or EXPAND_RIGHT"
        }
        expandDirection = direction
    }
}
expand_button/src/main/res/drawable/bg_search_bar.xml
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#80000000" />
    <corners android:radius="20dp" />
</shape>
expand_button/src/main/res/drawable/ic_search.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:fillColor="#FFffffff"
      android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>
expand_button/src/main/res/layout/layout_search_bar.xml
New file
@@ -0,0 +1,42 @@
<?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">
    <ImageView
        android:id="@+id/iv_search"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_centerInParent="true"
        android:src="@drawable/ic_search" />
    <EditText
        android:id="@+id/et_search"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/iv_search"
        android:layout_toStartOf="@id/tv_search"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:background="@null"
        android:hint="请输入分水房或取水口名称"
        android:textColorHint="#80FFFFFF"
        android:textColor="#FFFFFF"
        android:textSize="14sp"
        android:singleLine="true"
        android:visibility="gone"/>
    <TextView
        android:id="@+id/tv_search"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_centerVertical="true"
        android:layout_marginEnd="8dp"
        android:text="搜索"
        android:textColor="#FFFFFF"
        android:textSize="14sp"
        android:visibility="gone"/>
</RelativeLayout>
expand_button/src/main/res/values/attrs.xml
@@ -18,4 +18,17 @@
        <!-- 新增展开后的字体大小属性 -->
        <attr name="expandedTextSize" format="dimension"/>
    </declare-styleable>
    <declare-styleable name="CustomSearchBar">
        <!-- 使用android:background属性 -->
        <attr name="android:background"/>
        <!-- 搜索图标大小 -->
        <attr name="searchIconSize" format="dimension"/>
        <!-- 搜索按钮文字 -->
        <attr name="searchButtonText" format="string"/>
        <!-- 提示文字 -->
        <attr name="searchHint" format="string"/>
        <!-- 动画时长 -->
        <attr name="searchAnimDuration" format="integer"/>
    </declare-styleable>
</resources>