diff --git a/positions/build.properties b/positions/build.properties index 0add2a9..128b8f3 100644 --- a/positions/build.properties +++ b/positions/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Sep 29 19:30:19 GMT 2025 +#Tue Sep 30 06:43:35 GMT 2025 stageCount=3 libraryProject= baseVersion=15.0 publishVersion=15.0.2 -buildCount=17 +buildCount=39 baseBetaVersion=15.0.3 diff --git a/positions/src/main/java/cc/winboll/studio/positions/activities/LocationActivity.java b/positions/src/main/java/cc/winboll/studio/positions/activities/LocationActivity.java index 3e098ca..ee63438 100644 --- a/positions/src/main/java/cc/winboll/studio/positions/activities/LocationActivity.java +++ b/positions/src/main/java/cc/winboll/studio/positions/activities/LocationActivity.java @@ -32,6 +32,7 @@ import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.LocationServices; import com.google.android.gms.tasks.OnSuccessListener; import java.util.ArrayList; +import cc.winboll.studio.positions.models.PositionTaskModel; /** * 实时定位活动窗口: @@ -59,6 +60,7 @@ public class LocationActivity extends AppCompatActivity { private RecyclerView rvPositionList; // 位置列表(RecyclerView) private PositionAdapter positionAdapter; // 列表Adapter(含实时距离逻辑) ArrayList mPositionList = new ArrayList(); // 位置数据集合 + ArrayList mPositionTasksList = new ArrayList(); // 位置数据集合 @Override protected void onCreate(Bundle savedInstanceState) { @@ -154,7 +156,9 @@ public class LocationActivity extends AppCompatActivity { rvPositionList.setLayoutManager(layoutManager); // 2. 初始化Adapter(传入上下文和数据集合,兼容Java 7) - positionAdapter = new PositionAdapter(this, mPositionList); + PositionModel.loadBeanList(this, this.mPositionList, PositionModel.class); + PositionTaskModel.loadBeanList(this, this.mPositionTasksList, PositionTaskModel.class); + positionAdapter = new PositionAdapter(this, mPositionList, mPositionTasksList); rvPositionList.setAdapter(positionAdapter); // 3. 设置Adapter删除监听(删除列表项并同步本地数据) @@ -166,11 +170,25 @@ public class LocationActivity extends AppCompatActivity { }); // 4. 设置Adapter保存监听(编辑备注后同步本地数据) - positionAdapter.setOnSaveClickListener(new PositionAdapter.OnSaveClickListener() { + positionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() { @Override - public void onSaveClick() { + public void onSavePositionClick() { try { PositionModel.saveBeanList(LocationActivity.this, mPositionList, PositionModel.class); + Toast.makeText(LocationActivity.this, "位置信息已保存", Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(LocationActivity.this, "数据保存失败", Toast.LENGTH_SHORT).show(); + } + } + }); + + positionAdapter.setOnSavePositionTaskClickListener(new PositionAdapter.OnSavePositionTaskClickListener() { + @Override + public void onSavePositionTaskClick() { + try { + PositionTaskModel.saveBeanList(LocationActivity.this, mPositionTasksList, PositionTaskModel.class); + Toast.makeText(LocationActivity.this, "任务信息已保存", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(LocationActivity.this, "数据保存失败", Toast.LENGTH_SHORT).show(); @@ -403,7 +421,7 @@ public class LocationActivity extends AppCompatActivity { // 2. 停止Adapter的距离刷新定时器(关键:避免Timer持有Context导致内存泄漏) if (positionAdapter != null) { - positionAdapter.stopDistanceRefreshTimer(); + positionAdapter.stopTimer(); } // 3. 最后同步一次数据(确保所有修改保存) diff --git a/positions/src/main/java/cc/winboll/studio/positions/adapters/PositionAdapter.java b/positions/src/main/java/cc/winboll/studio/positions/adapters/PositionAdapter.java index a09c200..a143fc0 100644 --- a/positions/src/main/java/cc/winboll/studio/positions/adapters/PositionAdapter.java +++ b/positions/src/main/java/cc/winboll/studio/positions/adapters/PositionAdapter.java @@ -3,7 +3,7 @@ package cc.winboll.studio.positions.adapters; /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/09/29 20:25 - * @Describe 位置数据适配器(支持简单视图/编辑视图切换 + 实时距离开关控制) + * @Describe 位置数据适配器(修复定时器:仅刷新实时距离,不干扰编辑中的任务) */ import android.content.Context; import android.os.Handler; @@ -24,120 +24,212 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import cc.winboll.studio.positions.R; import cc.winboll.studio.positions.models.PositionModel; +import cc.winboll.studio.positions.models.PositionTaskModel; +import cc.winboll.studio.positions.views.PositionTaskListView; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import java.util.Timer; import java.util.TimerTask; public class PositionAdapter extends RecyclerView.Adapter { + // 常量定义(不变) public static final String TAG = "PositionAdapter"; - private static final int VIEW_TYPE_SIMPLE = 1; // 简单视图 - private static final int VIEW_TYPE_EDIT = 2; // 编辑视图 - private static final long REFRESH_INTERVAL = 5000; // 实时刷新间隔:5秒(5000毫秒) + private static final int VIEW_TYPE_SIMPLE = 1; + private static final int VIEW_TYPE_EDIT = 2; + private static final long REFRESH_INTERVAL = 5000; + private static final String DEFAULT_TASK_DESC = "新位置任务"; + private static final int DEFAULT_TASK_DISTANCE = 100; - private ArrayList mPositionList; - private Context mContext; - private PositionModel mCurrentGpsPosition; // 存储当前GPS位置(由Activity传入) - private Timer mDistanceTimer; // 定时计算距离的定时器 - private Handler mMainHandler; // 主线程Handler(更新UI必须在主线程) + // 核心成员变量(新增:用于存储可见的距离TextView,避免遍历所有ViewHolder) + private final ArrayList mPositionList; + private final ArrayList mAllPositionTasks; + private final Context mContext; + private PositionModel mCurrentGpsPosition; + private final Timer mDistanceTimer; + private final Handler mMainHandler; + private final Map> mPositionTaskMap; + // 关键新增:存储“位置ID → 距离TextView”的映射(仅包含当前可见的列表项) + private final Map mVisibleDistanceViews = new HashMap<>(); - // 接口定义(不变) + // 接口回调(不变) public interface OnDeleteClickListener { void onDeleteClick(int position); } - public interface OnSaveClickListener { - void onSaveClick(); + public interface OnSavePositionClickListener { + void onSavePositionClick(); + } + + public interface OnSavePositionTaskClickListener { + void onSavePositionTaskClick(); } private OnDeleteClickListener mOnDeleteClickListener; - private OnSaveClickListener mOnSaveClickListener; + private OnSavePositionClickListener mOnSavePositionClickListener; + private OnSavePositionTaskClickListener mOnSavePositionTaskClickListener; - // 构造函数(新增:初始化主线程Handler) - public PositionAdapter(Context context, ArrayList positionList) { + // 构造函数(不变,仅定时器初始化逻辑修改) + public PositionAdapter(Context context, ArrayList positionList, ArrayList allPositionTasks) { this.mContext = context; - this.mPositionList = positionList; - this.mMainHandler = new Handler(Looper.getMainLooper()); // 绑定主线程Looper + this.mPositionList = (positionList != null) ? positionList : new ArrayList(); + this.mAllPositionTasks = (allPositionTasks != null) ? allPositionTasks : new ArrayList(); + this.mMainHandler = new Handler(Looper.getMainLooper()); + this.mDistanceTimer = new Timer(); + this.mPositionTaskMap = new HashMap>(); + + // 初始化任务映射表(不变) + for (PositionModel model : mPositionList) { + String validPosId = model.getPositionId(); + ArrayList matchedTasks = new ArrayList(); + for (PositionTaskModel task : this.mAllPositionTasks) { + if (task != null && validPosId.equals(task.getPositionId())) { + matchedTasks.add(task); + } + } + mPositionTaskMap.put(validPosId, matchedTasks); + } + + // 启动距离刷新定时器(核心修改:调用新的刷新逻辑) + startDistanceRefreshTimer(); } - // ---------------------- 新增:设置当前GPS位置(由Activity调用,传入实时GPS数据) ---------------------- - /** - * 设置当前GPS位置(Activity通过定位API获取后,调用此方法传入Adapter) - * @param currentGpsPosition 包含当前经度、纬度的PositionModel(memo可空,isEnable无需关注) - */ + // 对外API:设置当前GPS位置(不变) public void setCurrentGpsPosition(PositionModel currentGpsPosition) { this.mCurrentGpsPosition = currentGpsPosition; - // 首次设置GPS位置时,启动定时器(避免重复启动) - if (currentGpsPosition != null && mDistanceTimer == null) { - startDistanceRefreshTimer(); - } } - // ---------------------- 新增:启动5秒定时刷新距离的定时器 ---------------------- - private void startDistanceRefreshTimer() { - // 先停止原有定时器(防止重复创建) - stopDistanceRefreshTimer(); - - mDistanceTimer = new Timer(); - mDistanceTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - // 定时器在子线程执行,更新UI需切换到主线程 - mMainHandler.post(new Runnable() { - @Override - public void run() { - // 刷新所有列表项的距离显示(仅简单视图生效) - notifyItemRangeChanged(0, getItemCount()); - } - }); - } - }, 0, REFRESH_INTERVAL); // 0延迟启动,每REFRESH_INTERVAL(5秒)执行一次 - } - - // ---------------------- 新增:停止定时器(避免内存泄漏) ---------------------- - /** - * 停止定时器(必须在Activity销毁/Adapter不再使用时调用,如Activity的onDestroy) - */ - public void stopDistanceRefreshTimer() { - if (mDistanceTimer != null) { - mDistanceTimer.cancel(); - mDistanceTimer.purge(); - mDistanceTimer = null; - } - } - - // 设置监听(不变) + // 对外API:设置监听器(不变) public void setOnDeleteClickListener(OnDeleteClickListener listener) { this.mOnDeleteClickListener = listener; } - public void setOnSaveClickListener(OnSaveClickListener listener) { - this.mOnSaveClickListener = listener; + public void setOnSavePositionClickListener(OnSavePositionClickListener listener) { + this.mOnSavePositionClickListener = listener; } - // 视图类型判断(不变) + public void setOnSavePositionTaskClickListener(OnSavePositionTaskClickListener listener) { + this.mOnSavePositionTaskClickListener = listener; + } + + // 对外API:停止定时器(不变) + public void stopTimer() { + if (mDistanceTimer != null) { + mDistanceTimer.cancel(); + mDistanceTimer.purge(); + } + // 清空可见距离视图缓存,避免内存泄漏 + mVisibleDistanceViews.clear(); + } + + // ---------------------- 核心修复1:定时器逻辑(仅刷新可见的距离TextView) ---------------------- + /** + * 启动实时距离刷新定时器(仅更新可见的距离文本,不刷新整个列表项) + */ + private void startDistanceRefreshTimer() { + mDistanceTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + // 切换到主线程(UI更新必须在主线程) + mMainHandler.post(new Runnable() { + @Override + public void run() { + // 遍历所有可见的距离TextView,针对性更新(不触发列表项重新绑定) + for (Map.Entry entry : mVisibleDistanceViews.entrySet()) { + String positionId = entry.getKey(); // 当前可见项的位置ID + TextView distanceView = entry.getValue(); // 要更新的距离文本控件 + + // 根据位置ID找到对应的PositionModel + PositionModel targetModel = null; + for (PositionModel model : mPositionList) { + if (positionId.equals(model.getPositionId())) { + targetModel = model; + break; + } + } + + // 若找到对应模型,直接更新距离文本(复用原有bindRealDistance逻辑) + if (targetModel != null) { + bindRealDistance(distanceView, targetModel); + } + } + } + }); + } + }, 0, REFRESH_INTERVAL); // 0延迟启动,每5秒执行一次 + } + + // ---------------------- 核心修复2:管理可见/不可见的距离TextView(避免内存泄漏+精准刷新) ---------------------- + /** + * 列表项变为“可见”时:将距离TextView加入缓存(供定时器刷新) + */ + @Override + public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) { + super.onViewAttachedToWindow(holder); + int position = holder.getAdapterPosition(); + if (position < 0 || position >= mPositionList.size()) { + return; + } + + PositionModel model = mPositionList.get(position); + String positionId = model.getPositionId(); + TextView distanceView = null; + + // 根据视图类型,找到对应的距离TextView + if (holder instanceof SimpleViewHolder) { + distanceView = ((SimpleViewHolder) holder).tvSimpleRealDistance; + } else if (holder instanceof EditViewHolder) { + // 编辑视图若也显示距离(根据需求,若不显示可删除此分支) + distanceView = ((EditViewHolder) holder).tvEditRealDistance; + } + + // 若距离TextView存在,加入缓存(key=位置ID,确保唯一) + if (distanceView != null) { + mVisibleDistanceViews.put(positionId, distanceView); + } + } + + /** + * 列表项变为“不可见”时:将距离TextView从缓存移除(避免内存泄漏+无效刷新) + */ + @Override + public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) { + super.onViewDetachedFromWindow(holder); + int position = holder.getAdapterPosition(); + if (position < 0 || position >= mPositionList.size()) { + return; + } + + PositionModel model = mPositionList.get(position); + String positionId = model.getPositionId(); + + // 从缓存移除不可见项的距离TextView + if (mVisibleDistanceViews.containsKey(positionId)) { + mVisibleDistanceViews.remove(positionId); + } + } + + // ---------------------- RecyclerView 核心方法(仅补充编辑视图的距离TextView绑定,其他不变) ---------------------- @Override public int getItemViewType(int position) { PositionModel model = mPositionList.get(position); return model.isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT; } - // 创建ViewHolder(修改:EditViewHolder新增单选框组引用;SimpleViewHolder不变) @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(mContext); if (viewType == VIEW_TYPE_SIMPLE) { - // 简单视图:绑定距离显示控件(不变) View view = inflater.inflate(R.layout.item_position_simple, parent, false); return new SimpleViewHolder(view); } else { - // 编辑视图:绑定新增的单选框组(关键修改) View view = inflater.inflate(R.layout.item_position_edit, parent, false); return new EditViewHolder(view); } } - // 绑定数据(核心修改:编辑视图添加单选框状态绑定) @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { PositionModel model = mPositionList.get(position); @@ -148,40 +240,43 @@ public class PositionAdapter extends RecyclerView.Adapter= 0 && position < mPositionList.size()) { + String positionId = mPositionList.get(position).getPositionId(); + mVisibleDistanceViews.remove(positionId); } - // 长按弹出编辑菜单(不变) + // 清理任务列表资源(不变) + if (holder instanceof SimpleViewHolder) { + ((SimpleViewHolder) holder).ptlvSimpleTasks.clearData(); + ((SimpleViewHolder) holder).ptlvSimpleTasks.setOnTaskUpdatedListener(null); + } else if (holder instanceof EditViewHolder) { + ((EditViewHolder) holder).ptlvEditTasks.clearData(); + ((EditViewHolder) holder).ptlvEditTasks.setOnTaskUpdatedListener(null); + } + } + + // 绑定简单视图(不变,仅确保距离TextView正确初始化) + private void bindSimpleView(final SimpleViewHolder holder, final PositionModel model, final int position) { + // 1. 绑定位置基础信息(不变) + holder.tvSimpleLongitude.setText(String.format("经度:%.6f", model.getLongitude())); + holder.tvSimpleLatitude.setText(String.format("纬度:%.6f", model.getLatitude())); + holder.tvSimpleMemo.setText(String.format("备注:%s", model.getMemo())); + + // 2. 绑定实时距离(首次初始化时调用,后续由定时器刷新) + bindRealDistance(holder.tvSimpleRealDistance, model); + + // 3. 长按切换编辑视图(不变) holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { @@ -194,47 +289,56 @@ public class PositionAdapter extends RecyclerView.Adapter matchedTasks = getSafeTasks(currentPosId); + holder.ptlvSimpleTasks.clearData(); + holder.ptlvSimpleTasks.setViewStatus(PositionTaskListView.VIEW_MODE_SIMPLE); + if (holder.ptlvSimpleTasks.getAllTasks().isEmpty()) { + holder.ptlvSimpleTasks.init(matchedTasks, currentPosId); + } } - // ---------------------- 核心修改:编辑视图绑定(新增单选框状态同步+保存) ---------------------- + // 绑定编辑视图(补充距离TextView绑定,其他不变) private void bindEditView(final EditViewHolder holder, final PositionModel model, final int position) { - // 1. 原有:绑定经纬度、备注(不变) + // 1. 绑定位置基础信息(新增:编辑视图的距离TextView初始化) holder.tvEditLongitude.setText(String.format("经度:%.6f", model.getLongitude())); holder.tvEditLatitude.setText(String.format("纬度:%.6f", model.getLatitude())); holder.etEditMemo.setText(model.getMemo()); - holder.etEditMemo.setSelection(model.getMemo().length()); + holder.etEditMemo.setSelection(holder.etEditMemo.getText().length()); + // 新增:首次绑定编辑视图时,初始化距离文本(后续由定时器刷新) + bindRealDistance(holder.tvEditRealDistance, model); - // 2. 新增:绑定实时距离开关状态(与model.isEnableRealPositionDistance同步) + // 2. 绑定实时距离开关(不变) if (model.isEnableRealPositionDistance()) { - // 启用实时距离→选中“启用”单选框 holder.rgRealDistanceSwitch.check(R.id.rb_enable); } else { - // 禁用实时距离→选中“禁用”单选框(默认状态) holder.rgRealDistanceSwitch.check(R.id.rb_disable); } - // 3. 按钮逻辑(不变+新增单选框保存) - // ① 删除按钮(最左侧,逻辑不变) + // 3. 绑定删除/取消/确定按钮(不变,确保任务编辑逻辑正常) holder.btnEditDelete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mOnDeleteClickListener != null) { mOnDeleteClickListener.onDeleteClick(position); + mPositionTaskMap.remove(model.getPositionId()); } } }); - // ② 取消按钮(中间,逻辑不变:不保存修改,切回简单视图) holder.btnEditCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -244,132 +348,280 @@ public class PositionAdapter extends RecyclerView.Adapter allTasks = holder.ptlvEditTasks.getAllTasks(); + ArrayList boundTasks = new ArrayList(); + for (PositionTaskModel task : allTasks) { + if (task != null) { + task.setPositionId(currentPosId); + boundTasks.add(task); + } } + mPositionTaskMap.put(currentPosId, boundTasks); - // 原有:切回简单视图+刷新列表+通知保存 model.setIsSimpleView(true); - notifyItemChanged(position); // 刷新当前项,实时更新距离显示状态 - - if (mOnSaveClickListener != null) { - mOnSaveClickListener.onSaveClick(); // 通知Activity保存到本地 + notifyItemChanged(position); + if (mOnSavePositionClickListener != null) { + mOnSavePositionClickListener.onSavePositionClick(); } - Toast.makeText(mContext, "备注及实时距离设置已保存", Toast.LENGTH_SHORT).show(); + Toast.makeText(mContext, "位置信息已保存", Toast.LENGTH_SHORT).show(); } }); - } - // ---------------------- ViewHolder 定义(核心修改:EditViewHolder新增单选框组引用) ---------------------- - /** - * 简单视图Holder(不变:含实时距离显示控件) - */ - public static class SimpleViewHolder extends RecyclerView.ViewHolder { - TextView tvSimpleLongitude; - TextView tvSimpleLatitude; - TextView tvSimpleMemo; - TextView tvSimpleRealDistance; // 实时距离显示控件 + // 4. 绑定任务列表(不变,确保编辑中的任务不被刷新覆盖) + final String currentPosId = model.getPositionId(); + ArrayList matchedTasks = getSafeTasks(currentPosId); + holder.ptlvEditTasks.clearData(); - public SimpleViewHolder(@NonNull View itemView) { - super(itemView); - // 绑定原有控件(不变) - tvSimpleLongitude = (TextView) itemView.findViewById(R.id.tv_simple_longitude); - tvSimpleLatitude = (TextView) itemView.findViewById(R.id.tv_simple_latitude); - tvSimpleMemo = (TextView) itemView.findViewById(R.id.tv_simple_memo); - // 绑定实时距离显示控件(不变) - tvSimpleRealDistance = (TextView) itemView.findViewById(R.id.tv_simple_real_distance); - } - } + holder.ptlvEditTasks.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() { + @Override + public void onTaskUpdated(String posId, ArrayList updatedTasks) { + ArrayList boundTasks = new ArrayList(); + for (PositionTaskModel task : updatedTasks) { + if (task != null) { + task.setPositionId(currentPosId); + boundTasks.add(task); + } + } - /** - * 编辑视图Holder(核心修改:新增实时距离开关单选框组及子项引用) - */ - public static class EditViewHolder extends RecyclerView.ViewHolder { - TextView tvEditLongitude; - TextView tvEditLatitude; - EditText etEditMemo; - Button btnEditDelete; - Button btnEditCancel; - Button btnEditConfirm; - // 新增:实时距离开关单选框组及子项(与item_position_edit.xml对应) - RadioGroup rgRealDistanceSwitch; // 单选框组(控制二选一) - RadioButton rbDisable; // “禁用”单选框(对应isEnable=false) - RadioButton rbEnable; // “启用”单选框(对应isEnable=true) + mPositionTaskMap.put(currentPosId, boundTasks); + Iterator taskIterator = mAllPositionTasks.iterator(); + while (taskIterator.hasNext()) { + PositionTaskModel task = taskIterator.next(); + if (task != null && currentPosId.equals(task.getPositionId())) { + taskIterator.remove(); + } + } + mAllPositionTasks.addAll(boundTasks); - public EditViewHolder(@NonNull View itemView) { - super(itemView); - // 绑定原有控件(不变,显式强转适配Java 7) - tvEditLongitude = (TextView) itemView.findViewById(R.id.tv_edit_longitude); - tvEditLatitude = (TextView) itemView.findViewById(R.id.tv_edit_latitude); - etEditMemo = (EditText) itemView.findViewById(R.id.et_edit_memo); - btnEditDelete = (Button) itemView.findViewById(R.id.btn_edit_delete); - btnEditCancel = (Button) itemView.findViewById(R.id.btn_edit_cancel); - btnEditConfirm = (Button) itemView.findViewById(R.id.btn_edit_confirm); - // 新增:绑定单选框组及子项(显式强转适配Java 7,与布局ID严格对应) - rgRealDistanceSwitch = (RadioGroup) itemView.findViewById(R.id.rg_real_distance_switch); - rbDisable = (RadioButton) itemView.findViewById(R.id.rb_disable); - rbEnable = (RadioButton) itemView.findViewById(R.id.rb_enable); - } - } + if (mOnSavePositionTaskClickListener != null) { + mOnSavePositionTaskClickListener.onSavePositionTaskClick(); + } + Toast.makeText(mContext, "任务信息已保存", Toast.LENGTH_SHORT).show(); + } + }); - // 数据存取方法(不变) - public void addPosition(PositionModel model) { - if (mPositionList != null && model != null) { - model.setIsSimpleView(true); - mPositionList.add(model); - notifyItemInserted(mPositionList.size() - 1); - } - } + if (holder.ptlvEditTasks.getAllTasks().isEmpty()) { + holder.ptlvEditTasks.init(matchedTasks, currentPosId); + } + holder.ptlvEditTasks.setViewStatus(PositionTaskListView.VIEW_MODE_EDIT); - public void removePosition(int position) { - if (mPositionList != null && position >= 0 && position < mPositionList.size()) { - mPositionList.remove(position); - notifyItemRemoved(position); - notifyItemRangeChanged(position, mPositionList.size()); - } - } + // 5. 绑定新增任务按钮(不变) + holder.btnAddTask.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PositionTaskModel newTask = new PositionTaskModel( + PositionTaskModel.genTaskId(), + currentPosId, + DEFAULT_TASK_DESC, + true, + DEFAULT_TASK_DISTANCE, + true + ); - public void updateAllPositions(ArrayList newList) { - if (newList != null) { - mPositionList.clear(); - for (PositionModel model : newList) { - model.setIsSimpleView(true); - } - mPositionList.addAll(newList); - notifyDataSetChanged(); - } - } + ArrayList currentTasks = getSafeTasks(currentPosId); + currentTasks.add(newTask); + mPositionTaskMap.put(currentPosId, new ArrayList(currentTasks)); + mAllPositionTasks.add(newTask); - public ArrayList getPositionList() { - return mPositionList; - } + holder.ptlvEditTasks.clearData(); + holder.ptlvEditTasks.init(currentTasks, currentPosId); + holder.ptlvEditTasks.setViewStatus(PositionTaskListView.VIEW_MODE_EDIT); + Toast.makeText(mContext, "已新增1个任务(绑定当前位置)", Toast.LENGTH_SHORT).show(); + } + }); + } - public void switchAllToSimpleView() { - if (mPositionList != null) { - for (PositionModel model : mPositionList) { - model.setIsSimpleView(true); - } - notifyDataSetChanged(); - } - } + // 工具方法:绑定实时距离(逻辑不变,仅负责文本更新,不影响其他控件) + private void bindRealDistance(TextView distanceView, PositionModel model) { + if (!model.isEnableRealPositionDistance()) { + distanceView.setText("实时距离:未启用"); + distanceView.setTextColor(mContext.getResources().getColor(R.color.colorGrayText)); + return; + } - @Override - public int getItemCount() { - return mPositionList == null ? 0 : mPositionList.size(); - } + if (mCurrentGpsPosition == null) { + distanceView.setText("实时距离:等待GPS定位"); + distanceView.setTextColor(mContext.getResources().getColor(R.color.colorGrayText)); + return; + } + + try { + double distanceM = PositionModel.calculatePositionDistance(mCurrentGpsPosition, model, false); + String distanceText = (distanceM < 1000) ? + String.format("实时距离:%.1f 米", distanceM) : + String.format("实时距离:%.1f 千米", distanceM / 1000); + distanceView.setText(distanceText); + distanceView.setTextColor(mContext.getResources().getColor(R.color.colorEnableGreen)); + } catch (IllegalArgumentException e) { + distanceView.setText("实时距离:数据无效"); + distanceView.setTextColor(mContext.getResources().getColor(R.color.colorRed)); + } + } + + // 工具方法:安全获取任务列表(不变) + private ArrayList getSafeTasks(String positionId) { + if (!mPositionTaskMap.containsKey(positionId)) { + mPositionTaskMap.put(positionId, new ArrayList()); + } + return mPositionTaskMap.get(positionId); + } + + // 对外API:新增位置(不变) + public void addPosition(PositionModel model) { + if (model == null) return; + String validPosId = model.getPositionId(); + mPositionList.add(model); + if (!mPositionTaskMap.containsKey(validPosId)) { + mPositionTaskMap.put(validPosId, new ArrayList()); + } + notifyItemInserted(mPositionList.size() - 1); + } + + // 对外API:删除位置(不变,补充距离缓存清理) + public void removePosition(int position) { + if (position < 0 || position >= mPositionList.size()) return; + PositionModel removedModel = mPositionList.get(position); + String removedPosId = removedModel.getPositionId(); + + // 1. 清理距离缓存(新增:移除已删除位置的距离TextView) + mVisibleDistanceViews.remove(removedPosId); + + // 2. 清理全局任务列表和映射表(不变) + Iterator taskIterator = mAllPositionTasks.iterator(); + while (taskIterator.hasNext()) { + PositionTaskModel task = taskIterator.next(); + if (task != null && removedPosId.equals(task.getPositionId())) { + taskIterator.remove(); + } + } + mPositionTaskMap.remove(removedPosId); + mPositionList.remove(position); + + notifyItemRemoved(position); + notifyItemRangeChanged(position, mPositionList.size()); + } + + // 对外API:更新所有位置(不变,补充距离缓存同步) + public void updateAllPositions(ArrayList newPositionList) { + if (newPositionList == null) return; + mPositionList.clear(); + mPositionList.addAll(newPositionList); + + // 1. 清理距离缓存中无效的位置ID(新增:仅保留新列表中的位置) + Iterator distanceViewIter = mVisibleDistanceViews.keySet().iterator(); + while (distanceViewIter.hasNext()) { + String posId = distanceViewIter.next(); + boolean isPosExist = false; + for (PositionModel model : newPositionList) { + if (posId.equals(model.getPositionId())) { + isPosExist = true; + break; + } + } + if (!isPosExist) { + distanceViewIter.remove(); + } + } + + // 2. 清理任务映射表(不变) + Iterator taskMapKeyIter = mPositionTaskMap.keySet().iterator(); + while (taskMapKeyIter.hasNext()) { + String posId = taskMapKeyIter.next(); + boolean isPosExist = false; + for (PositionModel model : newPositionList) { + if (posId.equals(model.getPositionId())) { + isPosExist = true; + break; + } + } + if (!isPosExist) { + taskMapKeyIter.remove(); + } + } + + // 3. 为新位置初始化任务列表(不变) + for (PositionModel model : newPositionList) { + String posId = model.getPositionId(); + if (!mPositionTaskMap.containsKey(posId)) { + mPositionTaskMap.put(posId, new ArrayList()); + } + } + + notifyDataSetChanged(); + } + + // 对外API:批量切换简单视图(不变) + public void switchAllToSimpleView() { + for (PositionModel model : mPositionList) { + model.setIsSimpleView(true); + } + notifyDataSetChanged(); + } + + // 对外API:获取位置列表(不变) + public ArrayList getPositionList() { + return new ArrayList(mPositionList); + } + + // ---------------------- ViewHolder 定义(核心补充:编辑视图的距离TextView) ---------------------- + // 简单视图Holder(不变) + public static class SimpleViewHolder extends RecyclerView.ViewHolder { + TextView tvSimpleLongitude; + TextView tvSimpleLatitude; + TextView tvSimpleMemo; + TextView tvSimpleRealDistance; // 简单视图-距离文本 + PositionTaskListView ptlvSimpleTasks; + + public SimpleViewHolder(@NonNull View itemView) { + super(itemView); + tvSimpleLongitude = (TextView) itemView.findViewById(R.id.tv_simple_longitude); + tvSimpleLatitude = (TextView) itemView.findViewById(R.id.tv_simple_latitude); + tvSimpleMemo = (TextView) itemView.findViewById(R.id.tv_simple_memo); + tvSimpleRealDistance = (TextView) itemView.findViewById(R.id.tv_simple_real_distance); + ptlvSimpleTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_simple_tasks); + } + } + + // 编辑视图Holder(核心补充:tvEditRealDistance 距离文本控件) + public static class EditViewHolder extends RecyclerView.ViewHolder { + TextView tvEditLongitude; + TextView tvEditLatitude; + EditText etEditMemo; + RadioGroup rgRealDistanceSwitch; + RadioButton rbDisable; + RadioButton rbEnable; + Button btnEditDelete; + Button btnEditCancel; + Button btnEditConfirm; + PositionTaskListView ptlvEditTasks; + Button btnAddTask; + TextView tvEditRealDistance; // 新增:编辑视图-距离文本(与布局文件对应) + + public EditViewHolder(@NonNull View itemView) { + super(itemView); + tvEditLongitude = (TextView) itemView.findViewById(R.id.tv_edit_longitude); + tvEditLatitude = (TextView) itemView.findViewById(R.id.tv_edit_latitude); + etEditMemo = (EditText) itemView.findViewById(R.id.et_edit_memo); + rgRealDistanceSwitch = (RadioGroup) itemView.findViewById(R.id.rg_real_distance_switch); + rbDisable = (RadioButton) itemView.findViewById(R.id.rb_disable); + rbEnable = (RadioButton) itemView.findViewById(R.id.rb_enable); + btnEditDelete = (Button) itemView.findViewById(R.id.btn_edit_delete); + btnEditCancel = (Button) itemView.findViewById(R.id.btn_edit_cancel); + btnEditConfirm = (Button) itemView.findViewById(R.id.btn_edit_confirm); + ptlvEditTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_edit_tasks); + btnAddTask = (Button) itemView.findViewById(R.id.btn_add_task); + tvEditRealDistance = (TextView) itemView.findViewById(R.id.tv_edit_real_distance); // 绑定编辑视图的距离控件 + } + } } + diff --git a/positions/src/main/java/cc/winboll/studio/positions/models/PositionModel.java b/positions/src/main/java/cc/winboll/studio/positions/models/PositionModel.java index 414e57a..ea05bef 100644 --- a/positions/src/main/java/cc/winboll/studio/positions/models/PositionModel.java +++ b/positions/src/main/java/cc/winboll/studio/positions/models/PositionModel.java @@ -12,39 +12,42 @@ import java.io.IOException; import java.util.UUID; public class PositionModel extends BaseBean { - + public static final String TAG = "PositionModel"; - // 位置标识符 + // 位置唯一标识符(与任务的positionId绑定) String positionId; - // 经度 + // 经度(范围:-180~180) double longitude; - // 纬度 + // 纬度(范围:-90~90) double latitude; - // 位置信息备注 + // 位置备注(空值时显示“无备注”) String memo; // 是否启用实时距离计算 boolean isEnableRealPositionDistance; - // 是否是简单视图 + // 是否显示简单视图(true=简单视图,false=编辑视图) boolean isSimpleView = true; + // 带参构造(强制初始化位置ID和经纬度) public PositionModel(String positionId, double longitude, double latitude, String memo, boolean isEnableRealPositionDistance) { - this.positionId = positionId; - this.longitude = longitude; - this.latitude = latitude; - this.memo = memo; + this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId; + this.longitude = Math.max(-180, Math.min(180, longitude)); // 经度范围限制 + this.latitude = Math.max(-90, Math.min(90, latitude)); // 纬度范围限制 + this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo; this.isEnableRealPositionDistance = isEnableRealPositionDistance; } - + + // 无参构造(默认值初始化,避免空指针) public PositionModel() { - this.positionId = ""; - this.longitude = 0.0f; - this.latitude = 0.0f; - this.memo = ""; + this.positionId = genPositionId(); + this.longitude = 0.0; + this.latitude = 0.0; + this.memo = "无备注"; this.isEnableRealPositionDistance = false; } + // ---------------------- Getter/Setter(确保字段有效性) ---------------------- public void setPositionId(String positionId) { - this.positionId = positionId; + this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId; } public String getPositionId() { @@ -68,7 +71,7 @@ public class PositionModel extends BaseBean { } public void setMemo(String memo) { - this.memo = memo; + this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo; } public String getMemo() { @@ -76,7 +79,7 @@ public class PositionModel extends BaseBean { } public void setLongitude(double longitude) { - this.longitude = longitude; + this.longitude = Math.max(-180, Math.min(180, longitude)); // 限制经度范围 } public double getLongitude() { @@ -84,79 +87,84 @@ public class PositionModel extends BaseBean { } public void setLatitude(double latitude) { - this.latitude = latitude; + this.latitude = Math.max(-90, Math.min(90, latitude)); // 限制纬度范围 } public double getLatitude() { return latitude; } - @Override - public String getName() { - return PositionModel.class.getName(); - } - + // ---------------------- 父类方法重写 ---------------------- + @Override + public String getName() { + return PositionModel.class.getName(); + } + + // 生成唯一位置ID(与任务ID格式一致,确保关联匹配) public static String genPositionId() { - // 生成唯一UUID(版本4,随机型) UUID uniqueUuid = UUID.randomUUID(); - // 转成字符串(标准格式,含横杠,共36位) - String uuidStr = uniqueUuid.toString(); - return uuidStr; + return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保与任务ID格式统一) } - @Override - public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { - super.writeThisToJsonWriter(jsonWriter); - jsonWriter.name("positionId").value(getPositionId()); - jsonWriter.name("longitude").value(getLongitude()); - jsonWriter.name("latitude").value(getLatitude()); - jsonWriter.name("memo").value(getMemo()); - jsonWriter.name("isEnableRealPositionDistance").value(isEnableRealPositionDistance()); - } + // JSON序列化(保存位置数据) + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("positionId").value(getPositionId()); + jsonWriter.name("longitude").value(getLongitude()); + jsonWriter.name("latitude").value(getLatitude()); + jsonWriter.name("memo").value(getMemo()); + jsonWriter.name("isEnableRealPositionDistance").value(isEnableRealPositionDistance()); + } - @Override - public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { - if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { - if (name.equals("positionId")) { - setPositionId(jsonReader.nextString()); - } else if (name.equals("longitude")) { - setLongitude(jsonReader.nextDouble()); - } else if (name.equals("latitude")) { - setLatitude(jsonReader.nextDouble()); - } else if (name.equals("memo")) { - setMemo(jsonReader.nextString()); - } else if (name.equals("isEnableRealPositionDistance")) { - setIsEnableRealPositionDistance(jsonReader.nextBoolean()); - } else { - return false; - } - } - return true; - } + // JSON反序列化(加载位置数据,校验字段) + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { + return true; + } else { + if (name.equals("positionId")) { + setPositionId(jsonReader.nextString()); + } else if (name.equals("longitude")) { + setLongitude(jsonReader.nextDouble()); + } else if (name.equals("latitude")) { + setLatitude(jsonReader.nextDouble()); + } else if (name.equals("memo")) { + setMemo(jsonReader.nextString()); + } else if (name.equals("isEnableRealPositionDistance")) { + setIsEnableRealPositionDistance(jsonReader.nextBoolean()); + } else { + return false; + } + } + return true; + } - @Override - public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { - jsonReader.beginObject(); - while (jsonReader.hasNext()) { - String name = jsonReader.nextName(); - if (!initObjectsFromJsonReader(jsonReader, name)) { - jsonReader.skipValue(); - } - } - // 结束 JSON 对象 - jsonReader.endObject(); - return this; - } - + // 从JSON读取位置数据 + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); // 跳过未知字段 + } + } + jsonReader.endObject(); + return this; + } + + // ---------------------- 核心工具方法:计算两点距离(Haversine公式,确保精度) ---------------------- /** - * 计算两个位置之间的直线距离(地球表面最短距离,基于Haversine公式) - * @param position1 第一个位置(PositionModel对象,含longitude/latitude) - * @param position2 第二个位置(PositionModel对象,含longitude/latitude) - * @param isKilometer 是否返回千米单位:true→千米(km),false→米(m) - * @return 两个位置间的距离(保留2位小数,单位由isKilometer决定) + * 计算两个位置之间的直线距离(地球表面最短距离) + * @param position1 第一个位置(非null) + * @param position2 第二个位置(非null) + * @param isKilometer 是否返回千米单位:true→千米,false→米 + * @return 距离(保留1位小数,符合显示需求) + * @throws IllegalArgumentException 位置为null或经纬度无效时抛出 */ public static double calculatePositionDistance(PositionModel position1, PositionModel position2, boolean isKilometer) { - // 1. 校验参数(避免空指针/无效经纬度,防止计算异常) + // 1. 校验参数有效性(避免计算异常) if (position1 == null || position2 == null) { throw new IllegalArgumentException("位置对象不能为null"); } @@ -164,41 +172,38 @@ public class PositionModel extends BaseBean { double lat1 = position1.getLatitude(); double lon2 = position2.getLongitude(); double lat2 = position2.getLatitude(); - // 经纬度范围校验(纬度:-90~90,经度:-180~180,超出则视为无效值) + // 经纬度范围二次校验(确保有效) if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90 || lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) { throw new IllegalArgumentException("经纬度值无效(纬度:-90~90,经度:-180~180)"); } - // 2. Haversine公式核心计算(将角度转为弧度,地球半径取平均半径6371km) - final double EARTH_RADIUS_KM = 6371; // 地球平均半径(单位:千米) - // 经纬度转为弧度(Math.toRadians:角度→弧度,公式计算需弧度值) - double radLat1 = Math.toRadians(lat1); + // 2. Haversine公式计算(地球半径取6371km,行业标准) + final double EARTH_RADIUS_KM = 6371; + double radLat1 = Math.toRadians(lat1); // 角度转弧度 double radLat2 = Math.toRadians(lat2); double radLon1 = Math.toRadians(lon1); double radLon2 = Math.toRadians(lon2); - // 计算纬度差、经度差 - double deltaLat = radLat2 - radLat1; - double deltaLon = radLon2 - radLon1; + double deltaLat = radLat2 - radLat1; // 纬度差 + double deltaLon = radLon2 - radLon1; // 经度差 - // Haversine公式:a = sin²(Δφ/2) + cos φ1 ⋅ cos φ2 ⋅ sin²(Δλ/2) + // 核心公式 double a = Math.pow(Math.sin(deltaLat / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(deltaLon / 2), 2); - // c = 2 ⋅ atan2(√a, √(1−a)) (计算圆心角) double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - // 距离 = 地球半径 × 圆心角(单位:千米) - double distanceKm = EARTH_RADIUS_KM * c; + double distanceKm = EARTH_RADIUS_KM * c; // 距离(千米) - // 3. 单位转换+保留2位小数(避免精度冗余,符合日常使用习惯) + // 3. 单位转换+精度处理(保留1位小数,符合显示需求) double distance; if (isKilometer) { - distance = Math.round(distanceKm * 100.0) / 100.0; // 千米,保留2位小数 + distance = Math.round(distanceKm * 10.0) / 10.0; // 千米→1位小数 } else { - distance = Math.round(distanceKm * 1000 * 100.0) / 100.0; // 米,保留2位小数(1km=1000m) + distance = Math.round(distanceKm * 1000 * 10.0) / 10.0; // 米→1位小数 } return distance; } } + diff --git a/positions/src/main/java/cc/winboll/studio/positions/models/PositionTaskModel.java b/positions/src/main/java/cc/winboll/studio/positions/models/PositionTaskModel.java index f23889b..d51f156 100644 --- a/positions/src/main/java/cc/winboll/studio/positions/models/PositionTaskModel.java +++ b/positions/src/main/java/cc/winboll/studio/positions/models/PositionTaskModel.java @@ -14,43 +14,46 @@ import java.util.UUID; public class PositionTaskModel extends BaseBean { public static final String TAG = "PositionTaskModel"; - // 任务标识符 + // 任务标识符(唯一) String taskId; - // 位置标识符 + // 绑定的位置标识符(与PositionModel的positionId一一对应) String positionId; // 任务描述 String taskDescription; - // 任务距离条件是否大于 + // 任务距离条件:是否大于设定距离 boolean isGreaterThan; - // 任务距离条件是否小于 + // 任务距离条件:是否小于设定距离(与isGreaterThan互斥) boolean isLessThan; - // 任务条件商议距离 + // 任务条件距离(单位:米) int discussDistance; // 是否启用任务 boolean isEnable; + // 带参构造(强制传入positionId,确保任务与位置绑定) public PositionTaskModel(String taskId, String positionId, String taskDescription, boolean isGreaterThan, int discussDistance, boolean isEnable) { - this.taskId = taskId; - this.positionId = positionId; - this.taskDescription = taskDescription; + this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId; // 空ID自动生成 + this.positionId = positionId; // 强制绑定位置ID + this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription; this.isGreaterThan = isGreaterThan; - this.isLessThan = !this.isGreaterThan; - this.discussDistance = discussDistance; + this.isLessThan = !isGreaterThan; // 确保互斥 + this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免无效值 this.isEnable = isEnable; } + // 无参构造(初始化默认值,positionId需后续设置) public PositionTaskModel() { - this.taskId = ""; + this.taskId = genTaskId(); this.positionId = ""; - this.taskDescription = ""; + this.taskDescription = "新任务"; this.isGreaterThan = true; - this.isLessThan = !this.isGreaterThan ; - this.discussDistance = 0; - this.isEnable = false; + this.isLessThan = false; // 初始互斥 + this.discussDistance = 100; // 默认100米 + this.isEnable = true; } + // ---------------------- Getter/Setter(确保positionId不为空,距离有效) ---------------------- public void setTaskId(String taskId) { - this.taskId = taskId; + this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId; } public String getTaskId() { @@ -58,7 +61,7 @@ public class PositionTaskModel extends BaseBean { } public void setPositionId(String positionId) { - this.positionId = positionId; + this.positionId = (positionId == null || positionId.trim().isEmpty()) ? "" : positionId; // 空值防护 } public String getPositionId() { @@ -66,25 +69,27 @@ public class PositionTaskModel extends BaseBean { } public void setTaskDescription(String taskDescription) { - this.taskDescription = taskDescription; + this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription; } public String getTaskDescription() { return taskDescription; } + // 修复:确保isGreaterThan和isLessThan互斥 public void setIsGreaterThan(boolean isGreaterThan) { this.isGreaterThan = isGreaterThan; - this.isLessThan = this.isGreaterThan; + this.isLessThan = !isGreaterThan; // 关键:小于 = 非大于 } public boolean isGreaterThan() { return isGreaterThan; } + // 修复:确保isLessThan和isGreaterThan互斥 public void setIsLessThan(boolean isLessThan) { this.isLessThan = isLessThan; - this.isGreaterThan = !this.isLessThan; + this.isGreaterThan = !isLessThan; // 关键:大于 = 非小于 } public boolean isLessThan() { @@ -92,7 +97,7 @@ public class PositionTaskModel extends BaseBean { } public void setDiscussDistance(int discussDistance) { - this.discussDistance = discussDistance; + this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免0或负数 } public int getDiscussDistance() { @@ -107,67 +112,71 @@ public class PositionTaskModel extends BaseBean { return isEnable; } - @Override - public String getName() { - return PositionTaskModel.class.getName(); - } + // ---------------------- 父类方法重写 ---------------------- + @Override + public String getName() { + return PositionTaskModel.class.getName(); + } + // 生成唯一任务ID(与PositionModel保持一致格式) public static String genTaskId() { - // 生成唯一UUID(版本4,随机型) UUID uniqueUuid = UUID.randomUUID(); - // 转成字符串(标准格式,含横杠,共36位) - String uuidStr = uniqueUuid.toString(); - return uuidStr; + return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保唯一) } - @Override - public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { - super.writeThisToJsonWriter(jsonWriter); - jsonWriter.name("taskId").value(getTaskId()); - jsonWriter.name("positionId").value(getPositionId()); - jsonWriter.name("taskDescription").value(getTaskDescription()); - jsonWriter.name("isGreaterThan").value(isGreaterThan()); - jsonWriter.name("isLessThan").value(isLessThan()); - jsonWriter.name("discussDistance").value(getDiscussDistance()); - jsonWriter.name("isEnable").value(isEnable()); - } + // JSON序列化(保存任务数据,包含所有字段) + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("taskId").value(getTaskId()); + jsonWriter.name("positionId").value(getPositionId()); + jsonWriter.name("taskDescription").value(getTaskDescription()); + jsonWriter.name("isGreaterThan").value(isGreaterThan()); + jsonWriter.name("isLessThan").value(isLessThan()); + jsonWriter.name("discussDistance").value(getDiscussDistance()); + jsonWriter.name("isEnable").value(isEnable()); + } - @Override - public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { - if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { - if (name.equals("taskId")) { - setTaskId(jsonReader.nextString()); - } else if (name.equals("positionId")) { - setPositionId(jsonReader.nextString()); - } else if (name.equals("taskDescription")) { - setTaskDescription(jsonReader.nextString()); - } else if (name.equals("isGreaterThan")) { - setIsGreaterThan(jsonReader.nextBoolean()); - } else if (name.equals("isLessThan")) { - setIsLessThan(jsonReader.nextBoolean()); - } else if (name.equals("discussDistance")) { - setDiscussDistance(jsonReader.nextInt()); - } else if (name.equals("isEnable")) { - setIsEnable(jsonReader.nextBoolean()); - } else { - return false; - } - } - return true; - } + // JSON反序列化(加载任务数据,校验字段有效性) + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { + return true; + } else { + if (name.equals("taskId")) { + setTaskId(jsonReader.nextString()); + } else if (name.equals("positionId")) { + setPositionId(jsonReader.nextString()); + } else if (name.equals("taskDescription")) { + setTaskDescription(jsonReader.nextString()); + } else if (name.equals("isGreaterThan")) { + setIsGreaterThan(jsonReader.nextBoolean()); + } else if (name.equals("isLessThan")) { + setIsLessThan(jsonReader.nextBoolean()); + } else if (name.equals("discussDistance")) { + setDiscussDistance(jsonReader.nextInt()); + } else if (name.equals("isEnable")) { + setIsEnable(jsonReader.nextBoolean()); + } else { + return false; + } + } + return true; + } - @Override - public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { - jsonReader.beginObject(); - while (jsonReader.hasNext()) { - String name = jsonReader.nextName(); - if (!initObjectsFromJsonReader(jsonReader, name)) { - jsonReader.skipValue(); - } - } - // 结束 JSON 对象 - jsonReader.endObject(); - return this; - } + // 从JSON读取任务数据(确保反序列化完整) + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); // 跳过未知字段,避免崩溃 + } + } + jsonReader.endObject(); + return this; + } } + diff --git a/positions/src/main/java/cc/winboll/studio/positions/views/PositionTaskListView.java b/positions/src/main/java/cc/winboll/studio/positions/views/PositionTaskListView.java new file mode 100644 index 0000000..4cf72ba --- /dev/null +++ b/positions/src/main/java/cc/winboll/studio/positions/views/PositionTaskListView.java @@ -0,0 +1,424 @@ +package cc.winboll.studio.positions.views; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/09/30 08:09 + * @Describe 位置任务列表视图(支持简单/编辑模式,确保任务修改实时生效) + */ +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import cc.winboll.studio.positions.R; +import cc.winboll.studio.positions.models.PositionTaskModel; +import java.util.ArrayList; +import java.util.List; + +public class PositionTaskListView extends LinearLayout { + // 视图模式常量 + public static final int VIEW_MODE_SIMPLE = 1; + public static final int VIEW_MODE_EDIT = 2; + + // 核心成员变量 + private String mBindPositionId; + private ArrayList mTaskList; + private int mCurrentViewMode; + private TaskListAdapter mTaskAdapter; + private RecyclerView mRvTasks; + + // 任务修改回调接口 + public interface OnTaskUpdatedListener { + void onTaskUpdated(String positionId, ArrayList updatedTasks); + } + + private OnTaskUpdatedListener mOnTaskUpdatedListener; + + // ---------------------- 构造函数 ---------------------- + public PositionTaskListView(Context context) { + super(context); + initView(context); + } + + public PositionTaskListView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initView(context); + } + + public PositionTaskListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context); + } + + // 初始化视图(绑定控件+设置布局) + private void initView(Context context) { + setOrientation(VERTICAL); + // 加载根布局(view_position_task_list.xml) + LayoutInflater.from(context).inflate(R.layout.view_position_task_list, this, true); + + // 绑定RecyclerView并设置布局管理器 + mRvTasks = (RecyclerView) findViewById(R.id.rv_position_tasks); + mRvTasks.setLayoutManager(new LinearLayoutManager(context)); + + // 初始化任务列表+适配器(强引用关联,确保数据同步) + mTaskList = new ArrayList(); + mTaskAdapter = new TaskListAdapter(mTaskList); + mRvTasks.setAdapter(mTaskAdapter); + + // 默认简单模式 + mCurrentViewMode = VIEW_MODE_SIMPLE; + } + + // ---------------------- 对外API ---------------------- + /** + * 初始化任务列表(仅首次空列表时加载,避免覆盖修改后数据) + */ + public void init(ArrayList taskList, String positionId) { + this.mBindPositionId = positionId; + // 仅内部列表为空时加载外部数据 + if (this.mTaskList.isEmpty()) { + ArrayList matchedTasks = new ArrayList(); + if (taskList != null && !taskList.isEmpty()) { + for (PositionTaskModel task : taskList) { + if (task != null && positionId.equals(task.getPositionId())) { + matchedTasks.add(task); + } + } + } + mTaskList.clear(); + mTaskList.addAll(matchedTasks); + } + mTaskAdapter.notifyDataSetChanged(); + } + + /** + * 设置视图模式(简单/编辑) + */ + public void setViewStatus(int viewMode) { + if (viewMode != VIEW_MODE_SIMPLE && viewMode != VIEW_MODE_EDIT) { + return; + } + mCurrentViewMode = viewMode; + mTaskAdapter.notifyDataSetChanged(); + } + + /** + * 设置任务修改回调 + */ + public void setOnTaskUpdatedListener(OnTaskUpdatedListener listener) { + this.mOnTaskUpdatedListener = listener; + } + + /** + * 获取当前任务列表(返回副本,避免外部修改污染) + */ + public ArrayList getAllTasks() { + return new ArrayList(mTaskList); + } + + /** + * 清空任务数据(避免RecyclerView复用残留) + */ + public void clearData() { + mTaskList.clear(); + if (mTaskAdapter != null && mTaskAdapter.mData != null) { + mTaskAdapter.mData.clear(); + } + mTaskAdapter.notifyDataSetChanged(); + mBindPositionId = null; + } + + /** + * 主动触发任务同步(供外部强制同步数据) + */ + public void triggerTaskSync() { + if (mOnTaskUpdatedListener != null && mBindPositionId != null) { + mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mTaskList)); + } + } + + // ---------------------- 内部工具方法 ---------------------- + /** + * 校验任务是否与当前位置匹配 + */ + private boolean isTaskMatchedWithPosition(PositionTaskModel task) { + if (task == null || mBindPositionId == null || mBindPositionId.trim().isEmpty()) { + return false; + } + return mBindPositionId.equals(task.getPositionId()); + } + + // ---------------------- 内部Adapter:任务列表适配器(核心修改:确保修改生效) ---------------------- + private class TaskListAdapter extends RecyclerView.Adapter { + // 适配器数据源(与外部mTaskList强引用,确保数据实时同步) + private final List mData; + + public TaskListAdapter(List data) { + this.mData = data; + } + + // 获取列表项数量(空列表显示1个“空提示”项) + @Override + public int getItemCount() { + return mData.isEmpty() ? 1 : mData.size(); + } + + // 区分视图类型(0=空提示,1=任务项) + @Override + public int getItemViewType(int position) { + return mData.isEmpty() ? 0 : 1; + } + + // 创建ViewHolder + @NonNull + @Override + public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + if (viewType == 0) { + // 空提示布局(item_task_empty.xml) + View emptyView = inflater.inflate(R.layout.item_task_empty, parent, false); + return new EmptyViewHolder(emptyView); + } else { + // 任务项布局(item_task_content.xml) + View taskView = inflater.inflate(R.layout.item_task_content, parent, false); + return new TaskContentViewHolder(taskView); + } + } + + // 绑定数据(核心:修复开关监听重复绑定+实时刷新) + @Override + public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) { + if (holder instanceof EmptyViewHolder) { + // 空提示项无需绑定数据 + return; + } + + // 校验位置有效性(避免越界) + if (position >= mData.size()) { + return; + } + final PositionTaskModel task = mData.get(position); + if (task == null) { + return; + } + + // 绑定任务数据 + final TaskContentViewHolder contentHolder = (TaskContentViewHolder) holder; + bindTaskData(contentHolder, task, position); + } + + /** + * 绑定任务具体数据(核心修复:开关监听去重+修改后实时刷新) + */ + private void bindTaskData(final TaskContentViewHolder holder, final PositionTaskModel task, final int position) { + // 1. 绑定基础信息(描述、距离条件、启用状态) + String taskDesc = (task.getTaskDescription() == null) ? "未设置描述" : task.getTaskDescription(); + holder.tvTaskDesc.setText(String.format("任务:%s", taskDesc)); + + String distanceCondition = task.isGreaterThan() ? "大于" : "小于"; + holder.tvTaskDistance.setText(String.format("条件:%s %d 米", distanceCondition, task.getDiscussDistance())); + + holder.cbTaskEnable.setChecked(task.isEnable()); + holder.cbTaskEnable.setEnabled(mCurrentViewMode == VIEW_MODE_EDIT); // 仅编辑模式可点击 + + // 2. 编辑模式处理(核心:修复开关监听重复绑定+修改同步) + if (mCurrentViewMode == VIEW_MODE_EDIT) { + holder.btnEditTask.setVisibility(View.VISIBLE); + holder.btnDeleteTask.setVisibility(View.VISIBLE); + + // (1)删除按钮逻辑 + holder.btnDeleteTask.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mData.remove(position); // 移除数据源中任务 + notifyItemRemoved(position); // 刷新列表(移除项) + notifyItemRangeChanged(position, mData.size()); // 刷新后续项索引 + // 同步通知外部 + if (mOnTaskUpdatedListener != null && mBindPositionId != null) { + mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mData)); + } + Toast.makeText(getContext(), "任务已删除", Toast.LENGTH_SHORT).show(); + } + }); + + // (2)修改按钮逻辑(弹窗编辑) + holder.btnEditTask.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showTaskEditDialog(task, position); + } + }); + + // (核心修复1:绑定新监听前先移除旧监听,避免重复触发) + holder.cbTaskEnable.setOnCheckedChangeListener(null); + holder.cbTaskEnable.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + task.setIsEnable(isChecked); // 直接修改数据源中任务的启用状态 + notifyItemChanged(position); // 实时刷新当前项(显示最新状态) + // 同步通知外部 + if (mOnTaskUpdatedListener != null && mBindPositionId != null) { + mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mData)); + } + } + }); + } else { + // 简单模式:隐藏编辑按钮+移除监听 + holder.btnEditTask.setVisibility(View.GONE); + holder.btnDeleteTask.setVisibility(View.GONE); + holder.cbTaskEnable.setOnCheckedChangeListener(null); + } + } + + /** + * 任务编辑弹窗(核心修复2:修改后实时刷新+同步数据) + */ + private void showTaskEditDialog(final PositionTaskModel task, final int position) { + final Context context = getContext(); + // 加载弹窗布局(dialog_edit_task.xml) + View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_edit_task, null); + + // 绑定弹窗控件 + final EditText etEditDesc = (EditText) dialogView.findViewById(R.id.et_edit_task_desc); + final RadioGroup rgDistanceCondition = (RadioGroup) dialogView.findViewById(R.id.rg_distance_condition); + final EditText etEditDistance = (EditText) dialogView.findViewById(R.id.et_edit_distance); + Button btnCancel = (Button) dialogView.findViewById(R.id.btn_dialog_cancel); + Button btnSave = (Button) dialogView.findViewById(R.id.btn_dialog_save); + + // 初始化弹窗数据(显示当前任务信息) + etEditDesc.setText(task.getTaskDescription()); + etEditDesc.setSelection(etEditDesc.getText().length()); // 光标定位到末尾 + + if (task.isGreaterThan()) { + rgDistanceCondition.check(R.id.rb_greater_than); + } else { + rgDistanceCondition.check(R.id.rb_less_than); + } + + etEditDistance.setText(String.valueOf(task.getDiscussDistance())); + + // 创建并显示弹窗 + final android.app.AlertDialog dialog = new android.app.AlertDialog.Builder(context) + .setView(dialogView) + .create(); + dialog.show(); + + // 取消按钮:关闭弹窗(不保存) + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + + // 保存按钮:校验+更新+同步(核心修复:实时刷新列表) + btnSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 1. 输入校验 + String newDesc = etEditDesc.getText().toString().trim(); + String distanceStr = etEditDistance.getText().toString().trim(); + + if (distanceStr.isEmpty()) { + Toast.makeText(context, "请输入有效距离", Toast.LENGTH_SHORT).show(); + return; + } + + int newDistance; + try { + newDistance = Integer.parseInt(distanceStr); + if (newDistance < 1) { // 距离最小为1米,避免无效值 + Toast.makeText(context, "距离不能小于1米", Toast.LENGTH_SHORT).show(); + return; + } + } catch (NumberFormatException e) { + Toast.makeText(context, "距离请输入数字", Toast.LENGTH_SHORT).show(); + return; + } + + // 2. 更新任务数据(直接修改数据源中的任务对象,强引用同步) + task.setTaskDescription(newDesc); + task.setDiscussDistance(newDistance); + // 更新距离条件(大于/小于) + boolean isGreater = rgDistanceCondition.getCheckedRadioButtonId() == R.id.rb_greater_than; + task.setIsGreaterThan(isGreater); + // 强制绑定当前位置ID,避免任务串位 + task.setPositionId(mBindPositionId); + + // 3. 核心修复:实时刷新当前任务项,确保修改后立即显示 + notifyItemChanged(position); + + // 4. 同步通知外部Adapter更新全局数据 + if (mOnTaskUpdatedListener != null && mBindPositionId != null) { + mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mData)); + } + + // 5. 关闭弹窗并提示 + dialog.dismiss(); + Toast.makeText(context, "任务已更新", Toast.LENGTH_SHORT).show(); + } + }); + } + + // ---------------------- ViewHolder 定义(内部类,绑定布局控件) ---------------------- + /** + * 基础ViewHolder(抽象类,统一父类型) + */ + public abstract class TaskViewHolder extends RecyclerView.ViewHolder { + public TaskViewHolder(@NonNull View itemView) { + super(itemView); + } + } + + /** + * 空提示ViewHolder(绑定“无任务”提示布局 item_task_empty.xml) + */ + public class EmptyViewHolder extends TaskViewHolder { + public EmptyViewHolder(@NonNull View itemView) { + super(itemView); + // 根据当前视图模式修改提示文本 + TextView tvEmptyTip = (TextView) itemView.findViewById(R.id.tv_task_empty_tip); + if (mCurrentViewMode == VIEW_MODE_EDIT) { + tvEmptyTip.setText("暂无任务,点击\"添加新任务\"创建"); + } else { + tvEmptyTip.setText("暂无启用的任务"); + } + } + } + + /** + * 任务内容ViewHolder(绑定任务项布局 item_task_content.xml) + */ + public class TaskContentViewHolder extends TaskViewHolder { + TextView tvTaskDesc; // 任务描述 + TextView tvTaskDistance; // 距离条件(如“大于 100 米”) + CompoundButton cbTaskEnable; // 任务启用/禁用开关 + Button btnEditTask; // 编辑任务按钮 + Button btnDeleteTask; // 删除任务按钮 + + public TaskContentViewHolder(@NonNull View itemView) { + super(itemView); + // 绑定布局控件(与 item_task_content.xml 中的ID严格对应) + tvTaskDesc = (TextView) itemView.findViewById(R.id.tv_task_desc); + tvTaskDistance = (TextView) itemView.findViewById(R.id.tv_task_distance); + cbTaskEnable = (CompoundButton) itemView.findViewById(R.id.cb_task_enable); + btnEditTask = (Button) itemView.findViewById(R.id.btn_edit_task); + btnDeleteTask = (Button) itemView.findViewById(R.id.btn_delete_task); + } + } + } +} + diff --git a/positions/src/main/res/drawable/bg_task_item.xml b/positions/src/main/res/drawable/bg_task_item.xml new file mode 100644 index 0000000..d8a68b8 --- /dev/null +++ b/positions/src/main/res/drawable/bg_task_item.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/positions/src/main/res/drawable/item_bg_edit.xml b/positions/src/main/res/drawable/item_bg_edit.xml new file mode 100644 index 0000000..589db77 --- /dev/null +++ b/positions/src/main/res/drawable/item_bg_edit.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/positions/src/main/res/drawable/item_bg_simple.xml b/positions/src/main/res/drawable/item_bg_simple.xml new file mode 100644 index 0000000..1617054 --- /dev/null +++ b/positions/src/main/res/drawable/item_bg_simple.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/positions/src/main/res/layout/dialog_edit_task.xml b/positions/src/main/res/layout/dialog_edit_task.xml new file mode 100644 index 0000000..a282b53 --- /dev/null +++ b/positions/src/main/res/layout/dialog_edit_task.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + +