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