app/build.gradle
@@ -89,7 +89,8 @@ } dependencies { implementation project(':library') implementation project(':expand_button') implementation(project(':library')) implementation project(':date_time_picker') implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.8.0' app/src/main/java/com/dayu/pipirrapp/dao/CenterPointDao.java
@@ -8,7 +8,9 @@ import androidx.room.Update; import com.dayu.pipirrapp.bean.db.CenterPointBean; import com.dayu.pipirrapp.bean.db.TagBean; import io.reactivex.rxjava3.core.Maybe; /** * author: zuo @@ -18,8 +20,11 @@ */ @Dao public interface CenterPointDao { @Query("SELECT * FROM CenterPointBean LIMIT 1") Maybe<CenterPointBean> findFirst(); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(CenterPointBean adminData); void insert(CenterPointBean centerPointBean); @Update void update(CenterPointBean adminData); @@ -27,6 +32,4 @@ @Delete void delete(CenterPointBean adminData); @Query("select * from CenterPointBean limit 1") CenterPointBean findFirst(); } app/src/main/java/com/dayu/pipirrapp/dao/DivideDao.java
@@ -14,6 +14,7 @@ import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Maybe; /** * DivideDao - @@ -50,4 +51,7 @@ @Query("select * from DivideBean") Single<List<DivideBean>> findAllToSingle(); @Query("SELECT * FROM divide") Maybe<List<DivideBean>> getAll(); // 改为返回Maybe<List<DivideBean>> } app/src/main/java/com/dayu/pipirrapp/dao/MarkerDao.java
@@ -13,6 +13,7 @@ import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Maybe; /** * author: zuo @@ -48,4 +49,8 @@ @Query("select * from MarkerBean") Single<List<MarkerBean>> findAllToSingle(); @Query("SELECT * FROM MarkerBean") Maybe<List<MarkerBean>> getAll(); } app/src/main/java/com/dayu/pipirrapp/fragment/MapFragment.java
@@ -78,6 +78,7 @@ import java.util.stream.Collectors; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; /** @@ -116,6 +117,9 @@ MarkerBean mMarkerBean; // 添加CompositeDisposable来管理所有订阅 private CompositeDisposable compositeDisposable = new CompositeDisposable(); @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -130,6 +134,13 @@ setRetainInstance(true); Log.i(TAG, "onCreate"); mInspectionState = SharedPreferencesHelper.getInstance(this.getContext()).get(CommonKeyName.inspectionState, 0); // 添加刷新数据的监听 LiveEventBus.get(CommonKeyName.refreshData).observe(this, o -> { getCenterPoint(); getMarkerData(); getDivideList(); }); } @Override @@ -142,7 +153,6 @@ @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentMapBinding.inflate(inflater, container, false); mapFragmenObserver.setmWebView(binding.webView); Log.i("MapFragment", "onCreateView"); @@ -151,15 +161,87 @@ MyWebViewInterface myWebViewInterface = new MyWebViewInterface(MapFragment.this); mWebView.addJavascriptInterface(myWebViewInterface, "Android"); mWebView.loadUrl("file:///android_asset/index.html"); getCenterPoint(); // 异步加载本地数据 loadLocalData(); initView(); initWeb(); getMarkerData(); getDivideList(); chageInspecState(mInspectionState); return binding.getRoot(); } /** * 异步加载本地数据 */ private void loadLocalData() { // 异步加载中心点数据 compositeDisposable.add( DaoSingleton.getAsynchInstance(this.getContext()).centerPointDao().findFirst() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { centerPointBean = result; if (centerPointBean == null) { getCenterPoint(); } else { jumpCenterPoint(); } }, throwable -> { Log.e(TAG, "Load centerPoint error: " + throwable); getCenterPoint(); }, () -> { // 当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(); }) ); // 异步加载分水房数据 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(); }) ); } private void initWeb() { @@ -232,9 +314,10 @@ * web加载完初始化本地数据 */ public void webFinishInitLocalData() { //跳转中心点 centerPointBean = DaoSingleton.getInstance(MapFragment.this.getContext()).centerPointDao().findFirst(); jumpCenterPoint(); // 只在本地没有数据时获取中心点 if (centerPointBean != null) { jumpCenterPoint(); } //添加因webview没有加载完成导致没有添加的地图标注 if (!webNoFinishMarkerData.isEmpty()) { for (MarkerBean bean : webNoFinishMarkerData) { @@ -285,18 +368,22 @@ }) .collect(Collectors.toList()); DaoSingleton.getInstance(MapFragment.this.getContext()).markerDao().deleteAll(); // 使用 RxJava 异步插入数据 DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).markerDao().insertAll(markerBeans) .subscribeOn(Schedulers.io()) // 在 IO 线程上执行 .observeOn(AndroidSchedulers.mainThread()) // 在主线程上观察 .subscribe(() -> { // 插入成功 Log.i("mWebView", "数据插入成功"); }, throwable -> { // 插入失败 Log.e("mWebView", "数据插入失败: " + throwable.getMessage()); }); // 使用 CompositeDisposable 管理数据库插入操作 compositeDisposable.add( DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).markerDao().insertAll(markerBeans) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( () -> { // 插入成功 Log.i("mWebView", "数据插入成功"); }, throwable -> { // 插入失败 Log.e("mWebView", "数据插入失败: " + throwable.getMessage()); } ) ); } } else { @@ -629,7 +716,7 @@ } /** * 添加取水口标注 * 添加管网标注 */ public void setMapDivide(PipeNetworkBean pipeNetworkBean) { // if (divide != null) { @@ -917,27 +1004,30 @@ try { if (t.isSuccess()) { if (t.getContent().getObj() != null && !t.getContent().getObj().isEmpty()) { List<DivideBean> divideBeans = new ArrayList<>(); for (DivideResult divideResult : t.getContent().getObj()) { DivideBean divideBean = getDivideBean(divideResult); setMapDivide(divideBean); divideBeans.add(divideBean); } // 使用 RxJava 异步插入数据 DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).divideDao().insertAll(divideBeans) .subscribeOn(Schedulers.io()) // 在 IO 线程上执行 .observeOn(AndroidSchedulers.mainThread()) // 在主线程上观察 .subscribe(() -> { // 插入成功 Log.i("mWebView", "数据插入成功"); }, throwable -> { // 插入失败 Log.e("mWebView", "数据插入失败: " + throwable.getMessage()); }); // 使用 CompositeDisposable 管理数据库插入操作 compositeDisposable.add( DaoSingleton.getAsynchInstance(MapFragment.this.getContext()).divideDao().insertAll(divideBeans) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( () -> { // 插入成功 Log.i("mWebView", "数据插入成功"); }, throwable -> { // 插入失败 Log.e("mWebView", "数据插入失败: " + throwable.getMessage()); } ) ); } } else { ToastUtil.showToastLong(MapFragment.this.getContext(), t.getMsg()); } @@ -946,7 +1036,6 @@ CrashReport.postCatchedException(e); } } }); } @@ -1031,6 +1120,10 @@ @Override public void onDestroy() { super.onDestroy(); // 清理所有订阅 if (compositeDisposable != null && !compositeDisposable.isDisposed()) { compositeDisposable.dispose(); } LiveEventBus.get(CommonKeyName.locationData).removeObserver(locationObserver); if (mWebView != null) { mWebView.destroy(); app/src/main/java/com/dayu/pipirrapp/fragment/MyFragment.java
@@ -24,6 +24,7 @@ import com.dayu.pipirrapp.utils.ToastUtil; import com.dayu.pipirrapp.view.ConfirmDialog; import com.dayu.pipirrapp.view.TipUtil; import com.jeremyliao.liveeventbus.LiveEventBus; /** * author: zuo @@ -94,6 +95,10 @@ Intent intent = new Intent(MyFragment.this.getContext(), IssueListActivity.class); MyFragment.this.getActivity().startActivity(intent); }); binding.refreshDataTV.setOnClickListener(v->{ // 发送刷新事件通知MapFragment刷新数据 LiveEventBus.get(CommonKeyName.refreshData).post(true); }); } private void initData() { app/src/main/java/com/dayu/pipirrapp/net/Constants.java
@@ -7,8 +7,8 @@ */ public class Constants { // public static final String BASE_URL = "http://192.168.10.52:8088"; // public static final String BASE_URL = "https://no253541tf71.vicp.fun"; public static final String BASE_URL = "http://192.168.40.166:54321"; public static final String BASE_URL = "https://no253541tf71.vicp.fun"; // public static final String BASE_URL = "http://192.168.40.166:54321"; //public static final String BASE_URL = "http://fve2iz.natappfree.cc"; public static final String BASE_UPLOAD_FILE_URL = BASE_URL; /** app/src/main/java/com/dayu/pipirrapp/utils/CommonKeyName.java
@@ -29,4 +29,8 @@ //创建通知 public final static String CreateNotification="CreateNotification"; /** * 刷新数据事件 */ public static final String refreshData = "refreshData"; } app/src/main/res/layout/fragment_map.xml
@@ -1,7 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"> <!-- <com.github.lzyzsd.jsbridge.BridgeWebView--> <!-- android:id="@+id/webView"--> @@ -105,7 +106,21 @@ android:textColor="@color/white" android:textSize="18sp" /> <com.example.expand_button.ExpandButton android:id="@+id/expandButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="10dp" android:background="@drawable/ic_green_bg" android:layout_alignParentRight="true" android:layout_marginTop="180dp" android:textSize="18sp" android:layout_marginRight="15dp" android:textColor="@color/white" app:letterSpacing="10dp" app:expandedText="点击展开" app:collapsedText="点" app:animDuration="300" /> <RelativeLayout android:id="@+id/pointRL" android:layout_width="match_parent" app/src/main/res/layout/fragment_my.xml
@@ -131,12 +131,37 @@ android:layout_marginRight="15dp" android:src="@drawable/ic_right" /> </RelativeLayout> <RelativeLayout android:id="@+id/refreshDataRL" android:layout_width="match_parent" android:layout_height="@dimen/item_height" android:layout_below="@+id/passwordRL" android:layout_marginTop="1dp"> <TextView android:id="@+id/refreshDataTV" android:layout_width="match_parent" android:layout_height="@dimen/item_height" android:background="@color/white" android:gravity="center_vertical" android:paddingLeft="30dp" android:text="更新本地数据" android:textColor="@color/black" android:textSize="@dimen/my_item_text_size" /> <ImageView android:layout_width="25dp" android:layout_height="25dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="15dp" android:src="@drawable/ic_right" /> </RelativeLayout> <RelativeLayout android:id="@+id/cleanDataRL" android:layout_width="match_parent" android:layout_height="@dimen/item_height" android:layout_below="@+id/passwordRL" android:layout_below="@+id/refreshDataRL" android:layout_marginTop="1dp"> <TextView expand_button/src/main/java/com/example/expand_button/ExpandButton.kt
@@ -1,14 +1,319 @@ package com.example.expand_button class ExpandButton { import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.ClickableSpan import android.util.AttributeSet import android.view.Gravity import android.view.MotionEvent import android.view.View import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.ContextCompat /** * 可展开的按钮控件 * 初始状态显示单个字符,点击后展开显示完整文字 * 展开后的每个字符都可以单独点击 */ class ExpandButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr) { // 展开时显示的完整文字 private var expandedText: String = "" // 收起时显示的单个字符 private var collapsedText: String = "" // 当前是否处于展开状态 private var isExpanded: Boolean = false // 动画持续时间,默认300毫秒 private var animationDuration: Long = 300 // 字符点击事件监听器 private var onCharClickListener: ((Char, Int) -> Unit)? = null // 字间距,默认为2dp private var customLetterSpacing: Float = context.resources.displayMetrics.density * 2 // 三角形图标 private val triangleDrawable: Drawable = ContextCompat.getDrawable( context, R.drawable.ic_triangle )!!.mutate() // 图标旋转角度 private var triangleRotation = 0f // 三角形图标与文字的间距,默认为8dp private var triangleMargin: Float = 3 * context.resources.displayMetrics.density init { // 读取自定义属性 context.theme.obtainStyledAttributes( attrs, R.styleable.ExpandButton, defStyleAttr, 0 ).apply { try { customLetterSpacing = getDimension(R.styleable.ExpandButton_letterSpacing, customLetterSpacing) expandedText = getString(R.styleable.ExpandButton_expandedText) ?: "" collapsedText = getString(R.styleable.ExpandButton_collapsedText) ?: "" animationDuration = getInteger(R.styleable.ExpandButton_animDuration, 300).toLong() triangleMargin = getDimension(R.styleable.ExpandButton_triangleMargin, triangleMargin) } finally { recycle() } } // 设置初始文本和宽度 if (collapsedText.isNotEmpty()) { text = collapsedText post { // 确保初始宽度为收起状态的宽度 layoutParams = layoutParams.apply { width = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight } } } // 设置文本可点击,仅在收起状态时响应点击展开 setOnClickListener { if (!isExpanded) { toggleExpand() } } // 添加触摸事件处理 setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { // 检查点击是否在三角形图标区域内 if (isClickOnTriangle(event.x)) { toggleExpand() return@setOnTouchListener true } } } false } // 设置左边距,为图标留出空间 compoundDrawablePadding = triangleMargin.toInt() setPadding( (16 * context.resources.displayMetrics.density + triangleMargin).toInt(), // 左边距增加,为图标留空间 paddingTop, paddingRight, paddingBottom ) // 设置单行显示,防止高度变化 maxLines = 1 isSingleLine = true // 设置文字垂直居中 gravity = Gravity.CENTER_VERTICAL } override fun onDraw(canvas: Canvas) { // 保存画布状态 canvas.save() // 计算图标位置 val iconSize = triangleDrawable.intrinsicWidth val iconLeft = paddingLeft - iconSize - compoundDrawablePadding val iconTop = (height - iconSize) / 2 // 设置图标边界 triangleDrawable.setBounds( iconLeft, iconTop, iconLeft + iconSize, iconTop + iconSize ) // 旋转画布 canvas.rotate( triangleRotation, (iconLeft + iconSize / 2).toFloat(), (iconTop + iconSize / 2).toFloat() ) // 绘制图标 triangleDrawable.draw(canvas) // 恢复画布状态 canvas.restore() super.onDraw(canvas) } /** * 设置字间距 * @param spacing 间距值(像素) */ fun setCustomLetterSpacing(spacing: Float) { this.customLetterSpacing = spacing if (isExpanded) { setExpandedClickableText() } } /** * 设置展开和收起时显示的文字 * @param expanded 展开时显示的完整文字 * @param collapsed 收起时显示的单个字符 */ fun setExpandText(expanded: String, collapsed: String) { this.expandedText = expanded this.collapsedText = collapsed text = collapsedText } /** * 设置单个字符点击监听器 * @param listener 点击回调,参数为被点击的字符和位置 * char: 被点击的字符 * position: 字符在文本中的位置(从0开始) */ fun setOnCharClickListener(listener: (char: Char, position: Int) -> Unit) { this.onCharClickListener = listener } /** * 设置展开/收起动画的持续时间 * @param duration 动画持续时间(毫秒) */ fun setAnimationDuration(duration: Long) { this.animationDuration = duration } /** * 切换展开/收起状态 * 使用ValueAnimator实现宽度动画和图标旋转 */ private fun toggleExpand() { isExpanded = !isExpanded // 计算收起和展开状态的宽度 val collapsedWidth = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight val expandedWidth = calculateExpandedWidth() // 创建宽度动画 ValueAnimator.ofInt( if (isExpanded) collapsedWidth else expandedWidth, if (isExpanded) expandedWidth else collapsedWidth ).apply { duration = animationDuration addUpdateListener { animator -> layoutParams = layoutParams.apply { width = animator.animatedValue as Int } requestLayout() } start() } // 创建图标旋转动画 ValueAnimator.ofFloat( if (isExpanded) 0f else 180f, if (isExpanded) 180f else 0f ).apply { duration = animationDuration addUpdateListener { animator -> triangleRotation = animator.animatedValue as Float invalidate() // 重绘以更新图标旋转 } start() } // 更新文本 if (isExpanded) { setExpandedClickableText() } else { text = collapsedText } } /** * 计算展开后的总宽度 */ private fun calculateExpandedWidth(): Int { val spaceWidth = paint.measureText(" ") * (customLetterSpacing / 10) // 计算所有字符的总宽度 val textWidth = expandedText.fold(0f) { acc, char -> acc + paint.measureText(char.toString()) } // 计算间距的总宽度(字符数量减1个间距) val spacesWidth = spaceWidth * (expandedText.length - 1) return (textWidth + spacesWidth).toInt() + paddingLeft + paddingRight } /** * 设置展开后的可点击文本 */ private fun setExpandedClickableText() { val builder = SpannableStringBuilder() expandedText.forEachIndexed { index, char -> // 添加字符 builder.append(char) // 为字符设置点击事件 val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) { onCharClickListener?.invoke(char, index) } override fun updateDrawState(ds: android.text.TextPaint) { super.updateDrawState(ds) // 移除下划线 ds.isUnderlineText = false // 保持原始文字颜色 ds.color = currentTextColor } } builder.setSpan( clickableSpan, builder.length - 1, builder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) // 只在非最后一个字符后添加空格作为间距 if (index < expandedText.length - 1) { builder.append(" ".repeat((customLetterSpacing / 10).toInt())) } } text = builder // 启用LinkMovementMethod以响应ClickableSpan的点击事件 movementMethod = android.text.method.LinkMovementMethod.getInstance() } /** * 判断点击是否在三角形图标区域内 */ private fun isClickOnTriangle(x: Float): Boolean { val iconSize = triangleDrawable.intrinsicWidth val iconLeft = paddingLeft - iconSize - compoundDrawablePadding return x <= paddingLeft && x >= iconLeft } /** * 设置三角形图标与文字的间距 * @param margin 间距值(像素) */ fun setTriangleMargin(margin: Float) { this.triangleMargin = margin compoundDrawablePadding = margin.toInt() setPadding( (16 * context.resources.displayMetrics.density + margin).toInt(), paddingTop, paddingRight, paddingBottom ) } } expand_button/src/main/res/drawable/ic_triangle.xml
New file @@ -0,0 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="16dp" android:height="16dp" android:viewportWidth="48" android:viewportHeight="48"> <path android:pathData="M19,12L31,24L19,36" android:strokeLineJoin="round" android:strokeWidth="4" android:fillColor="#00000000" android:strokeColor="#757575" android:strokeLineCap="round"/> </vector> expand_button/src/main/res/values/attrs.xml
New file @@ -0,0 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="ExpandButton"> <!-- 字间距 --> <attr name="letterSpacing" format="dimension"/> <!-- 展开时显示的文字 --> <attr name="expandedText" format="string"/> <!-- 收起时显示的文字 --> <attr name="collapsedText" format="string"/> <!-- 动画时长 --> <attr name="animDuration" format="integer"/> <!-- 三角形图标与文字的间距 --> <attr name="triangleMargin" format="dimension"/> </declare-styleable> </resources>