1.web地图添加管网显示。
2.安卓原生添加管网信息的获取。
3.安卓原生添加管网信息的本地持久化(SQLite数据库)。
4.实现图例用户的选择状态按钮的持久化。
5.实现自定义搜索按钮的实现。
6.实现搜索后弹出界面的相关功能开发。
18个文件已添加
13个文件已修改
1个文件已删除
New file |
| | |
| | | <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> |
| | |
| | | |
| | | // 手机获取到定位后显示定位 |
| | | 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, |
| | |
| | | //设置地图中心点 |
| | | 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); |
| | | } |
| | | |
| | |
| | | if (lastClickedMarker !== null) { |
| | | lastClickedMarker.setIcon(createIcon(CONFIG.IMAGES.MARKER_BLUE)); |
| | | } |
| | | if(lastClickedDivide!==null) |
| | | { |
| | | if (lastClickedDivide !== null) { |
| | | lastClickedDivide.setIcon(createIcon(CONFIG.IMAGES.DIVIDE_BLUE)); |
| | | } |
| | | if (isShowWaterIntakeDetail || isShowDivideDetail) { |
| | |
| | | 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), |
| | |
| | | marker: marker, |
| | | label: label |
| | | }); |
| | | |
| | | if (isShow === "true" || isShow === true) { |
| | | map.addOverLay(label); |
| | | map.addOverLay(marker); |
| | | } |
| | | return "addMarker加载成功 id:" + id |
| | | } |
| | | //更新位坐标 |
| | |
| | | |
| | | // 管网线路管理 |
| | | 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(); |
| | | } |
| | | }, |
| | | // 修改判断逻辑,确保字符串"false"被正确处理 |
| | | const shouldCreateNewLine = isNewLine === true || isNewLine === "true" || !this.currentLine.overlay; |
| | | |
| | | /** |
| | | * 更新当前线路的显示 |
| | | */ |
| | | updateCurrentLineDisplay() { |
| | | if (!this.currentLine.overlay) { |
| | | // 创建新的线路图层 |
| | | this.currentLine.overlay = new T.Polyline(this.currentLine.points, this.style); |
| | | 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 { |
| | | // 更新现有线路的点 |
| | | // 添加点到现有线路 |
| | | this.currentLine.points.push(point); |
| | | this.currentLine.overlay.setLngLats(this.currentLine.points); |
| | | |
| | | } |
| | | // 如果是新线路,将之前的线路保存到 lines 数组 |
| | | if (isNewLine === true || isNewLine === "true") { |
| | | this.lines.push({ |
| | | points: [...this.currentLine.points], |
| | | overlay: this.currentLine.overlay |
| | | }); |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * 完成当前线路 |
| | | */ |
| | | finishCurrentLine() { |
| | | if (this.currentLine.points.length > 1) { |
| | | if (this.currentLine.overlay) { |
| | | // 将当前线路添加到完成列表 |
| | | this.lines.push(this.currentLine.overlay); |
| | | } |
| | | } else if (this.currentLine.overlay) { |
| | | // 如果点数不足,清除图层 |
| | | map.removeOverLay(this.currentLine.overlay); |
| | | } |
| | | |
| | | // 重置当前线路 |
| | | this.currentLine = { |
| | | points: [], |
| | | overlay: null |
| | | }; |
| | | }, |
| | | |
| | | /** |
| | | * 显示所有线路 |
| | | */ |
| | | 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 = []; |
| | |
| | | /** |
| | | * 添加管网线路点 |
| | | */ |
| | | function addPipeNetwork(lng, lat, isNewLine) { |
| | | PipelineManager.addPoint(lng, lat, isNewLine); |
| | | function addPipeNetwork(lng, lat, isNewLine, isShow) { |
| | | PipelineManager.addPoint(lng, lat, isNewLine, isShow); |
| | | } |
| | | |
| | | /** |
| | |
| | | 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 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; |
| | |
| | | marker: marker, |
| | | label: label |
| | | }); |
| | | |
| | | if (isShow === true || isShow === "true") { |
| | | map.addOverLay(label); |
| | | map.addOverLay(marker); // 将标注添加到地图中 |
| | | } |
| | | return "addMarker加载成功 id:" + id |
| | | } |
| | | // 修改点击标注的图标 |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | | |
| | |
| | | * @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; |
| | |
| | | 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; |
| | | } |
| | | |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | List<Coordinate> coordinates; |
| | | String type; |
| | | String networkId; |
| | | |
| | | |
| | | |
| | | public List<Coordinate> getCoordinates() { |
| | | return coordinates; |
| | |
| | | this.type = type; |
| | | } |
| | | |
| | | public String getNetworkId() { |
| | | return networkId; |
| | | } |
| | | |
| | | public void setNetworkId(String networkId) { |
| | | this.networkId = networkId; |
| | | } |
| | | } |
| | | |
| | | public String getType() { |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | @Delete |
| | | void delete(CenterPointBean adminData); |
| | | |
| | | @Query("DELETE FROM CenterPointBean") |
| | | void deleteAll(); |
| | | |
| | | } |
| | |
| | | |
| | | 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; |
| | | |
| | |
| | | |
| | | @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); |
| | | } |
| | |
| | | @Query("SELECT * FROM MarkerBean") |
| | | Maybe<List<MarkerBean>> getAll(); |
| | | |
| | | @Query("SELECT * FROM MarkerBean WHERE name LIKE '%' || :name || '%'") |
| | | Single<List<MarkerBean>> findByNameLike(String name); |
| | | } |
| | |
| | | package com.dayu.pipirrapp.dao; |
| | | |
| | | import androidx.room.Dao; |
| | | import androidx.room.Delete; |
| | | import androidx.room.Insert; |
| | | import androidx.room.OnConflictStrategy; |
| | |
| | | /** |
| | | * 管网相关dao |
| | | */ |
| | | @Dao |
| | | public interface PipeNetDao { |
| | | @Insert(onConflict = OnConflictStrategy.REPLACE) |
| | | void insert(PipeNetworkBean pipeNetworkBean); |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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 |
| | |
| | | //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<>(); |
| | | //所有分水房 |
| | |
| | | public double centerLat; |
| | | |
| | | MarkerBean mMarkerBean; |
| | | |
| | | //是否显示Marker |
| | | boolean isShowMarker, isShowPipeNetwork, isShowDivide; |
| | | |
| | | // 添加CompositeDisposable来管理所有订阅 |
| | | private CompositeDisposable compositeDisposable = new CompositeDisposable(); |
| | |
| | | * 异步加载本地数据 |
| | | */ |
| | | private void loadLocalData() { |
| | | |
| | | |
| | | // 异步加载中心点数据 |
| | | compositeDisposable.add( |
| | | DaoSingleton.getAsynchInstance(this.getContext()).centerPointDao().findFirst() |
| | |
| | | getCenterPoint(); |
| | | }) |
| | | ); |
| | | |
| | | isShowMarker = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.markerKeyIsShow, true); |
| | | // 异步加载取水口数据 |
| | | compositeDisposable.add( |
| | | DaoSingleton.getAsynchInstance(this.getContext()).markerDao().getAll() |
| | |
| | | getMarkerData(); |
| | | }) |
| | | ); |
| | | |
| | | isShowDivide = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.divideIsShow, true); |
| | | // 异步加载分水房数据 |
| | | compositeDisposable.add( |
| | | DaoSingleton.getAsynchInstance(this.getContext()).divideDao().getAll() |
| | |
| | | getDivideList(); |
| | | }) |
| | | ); |
| | | |
| | | isShowPipeNetwork = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.pipeNetworkIsShow, true); |
| | | //异步加载管网数据 |
| | | compositeDisposable.add( |
| | | DaoSingleton.getAsynchInstance(this.getContext()).pipeNetDao().getAll() |
| | |
| | | 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(); |
| | | }) |
| | | ); |
| | | } |
| | |
| | | setMapDivide(bean); |
| | | } |
| | | } |
| | | if (!webNoFinishPipeNetworkData.isEmpty()) { |
| | | for (PipeNetworkBean bean : webNoFinishPipeNetworkData) { |
| | | setMapPipe(bean); |
| | | } |
| | | } |
| | | |
| | | //显示所有取水口 |
| | | MarkerUtils.showLocoMarks(MapFragment.this); |
| | | //显示巡检状态并且显示因意外关闭的历史数据 |
| | |
| | | return markerBean; |
| | | }) |
| | | .collect(Collectors.toList()); |
| | | |
| | | DaoSingleton.getInstance(MapFragment.this.getContext()).markerDao().deleteAll(); |
| | | // 使用 CompositeDisposable 管理数据库插入操作 |
| | | compositeDisposable.add( |
| | | DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).markerDao().insertAll(markerBeans) |
| | |
| | | //巡检按钮 |
| | | 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(); |
| | |
| | | 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; |
| | | }); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 显示取水口详情 |
| | |
| | | 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 { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 添加标注点 |
| | | * 添加取水口 |
| | | */ |
| | | 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) { |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 添加取水口标注 |
| | | * 添加分水房 |
| | | */ |
| | | 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) { |
| | | } |
| | |
| | | /** |
| | | * 添加管网标注 |
| | | */ |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | setMapDivide(divideBean); |
| | | divideBeans.add(divideBean); |
| | | } |
| | | |
| | | DaoSingleton.getInstance(MapFragment.this.getContext()).divideDao().deleteAll(); |
| | | // 使用 CompositeDisposable 管理数据库插入操作 |
| | | compositeDisposable.add( |
| | | DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).divideDao().insertAll(divideBeans) |
| | |
| | | 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 异步插入数据 |
| | | compositeDisposable.add( |
| | | DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).pipeNetDao().insertAll(pipeNetBeans) |
| | | .subscribeOn(Schedulers.io()) // 在 IO 线程上执行 |
| | | .observeOn(AndroidSchedulers.mainThread()) // 在主线程上观察 |
| | |
| | | }, throwable -> { |
| | | // 插入失败 |
| | | Log.e("mWebView", "数据插入失败: " + throwable.getMessage()); |
| | | }); |
| | | }) |
| | | ); |
| | | } |
| | | |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | private void showPipeLine(boolean isSelected) { |
| | | if (isSelected) { |
| | | mWebView.evaluateJavascript("javascript:showAllPipeLines()", value -> { |
| | | }); |
| | | } else { |
| | | mWebView.evaluateJavascript("javascript:hideAllPipeLines()", value -> { |
| | | }); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | * 刷新数据事件 |
| | | */ |
| | | 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"; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <?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> |
| | |
| | | </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" /> |
| | |
| | | <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" /> |
| | | |
| | |
| | | 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" |
New file |
| | |
| | | <?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> |
| | |
| | | <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> |
| | | |
| | | |
| | | |
| | | |
New file |
| | |
| | | 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() |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | |
| | | // 添加一个 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) |
| | | } |
| | | |
| | | /** |
| | |
| | | index, |
| | | legendItems[index].isSelected |
| | | ) |
| | | |
| | | // 保存状态 |
| | | saveStates() |
| | | |
| | | invalidate() |
| | | } |
New file |
| | |
| | | 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 |
| | | } |
| | | } |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <?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> |
| | |
| | | <!-- 新增展开后的字体大小属性 --> |
| | | <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> |