改进GPS订阅服务发送框架

This commit is contained in:
2026-05-07 14:37:07 +08:00
parent 9c16685c1f
commit e24c9bdce3
4 changed files with 301 additions and 340 deletions

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.libgpsrelaysentinel.manager;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:26
*/
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeConst;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
import cc.winboll.studio.libgpsrelaysentinel.model.LocationPoint;
@@ -15,12 +10,18 @@ import java.util.Map;
public final class SubscribeLocationManager {
private static SubscribeLocationManager instance;
//订阅配置
private final Map<String,GpsSubscribeMsg> subscribeConfigMap;
//基准定点坐标
private final Map<String,LocationPoint> subscriberPointMap;
//真实推送计数(精准统计)
private final Map<String,Integer> subscriberPushCountMap;
private SubscribeLocationManager(){
subscribeConfigMap = new HashMap<String, GpsSubscribeMsg>();
subscriberPointMap = new HashMap<String, LocationPoint>();
subscriberPushCountMap = new HashMap<String, Integer>();
}
public static SubscribeLocationManager getInstance(){
@@ -30,48 +31,70 @@ public final class SubscribeLocationManager {
return instance;
}
public void putSubscribeConfig(final String sid,final GpsSubscribeMsg msg){
//========= 订阅配置 =========
public void putSubscribeConfig(String sid,GpsSubscribeMsg msg){
subscribeConfigMap.put(sid,msg);
}
public void initSubscriberPoint(final String sid,double lat,double lng){
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
}
public void updateSubscriberPoint(final String sid,double lat,double lng){
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
}
public LocationPoint getLastPoint(final String sid){
return subscriberPointMap.get(sid);
}
public GpsSubscribeMsg getSubscribeConfig(final String sid){
public GpsSubscribeMsg getSubscribeConfig(String sid){
return subscribeConfigMap.get(sid);
}
public boolean isNeedPush(final String sid,double nowLat,double nowLng){
//========= 基准定点坐标 =========
public void initSubscriberPoint(String sid,double lat,double lng){
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
}
public void updateSubscriberPoint(String sid,double lat,double lng){
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
}
public LocationPoint getLastPoint(String sid){
return subscriberPointMap.get(sid);
}
//========= 精准推送计数 =========
public void addPushCount(String sid){
int current = subscriberPushCountMap.get(sid) == null ? 0 : subscriberPushCountMap.get(sid);
subscriberPushCountMap.put(sid,current + 1);
}
public int getPushCount(String sid){
return subscriberPushCountMap.get(sid) == null ? 0 : subscriberPushCountMap.get(sid);
}
public void clearPushCount(String sid){
subscriberPushCountMap.put(sid,0);
}
//========= 步长规则判断 =========
public boolean isNeedPush(String sid,double nowLat,double nowLng){
GpsSubscribeMsg config = getSubscribeConfig(sid);
if(config == null){
return false;
}
//全量订阅直接放行
if(config.getSubscribeMode() == GpsSubscribeConst.SUB_TYPE_ALL){
return true;
}
//无初始定点 → 先建立第一个基准点
LocationPoint lastPoint = getLastPoint(sid);
if(lastPoint == null){
return true;
}
//计算实际移动距离
double distance = calculateDistance(
lastPoint.getLatitude(),lastPoint.getLongitude(),
nowLat,nowLng
);
return distance >= config.getStepDistanceM();
}
//两点经纬度距离计算(米)
private double calculateDistance(double lat1,double lng1,double lat2,double lng2){
double radLat1 = Math.toRadians(lat1);
double radLat2 = Math.toRadians(lat2);
@@ -81,23 +104,25 @@ public final class SubscribeLocationManager {
double latDiff = radLat1 - radLat2;
double lngDiff = radLng1 - radLng2;
double result = 2 * Math.asin(Math.sqrt(
Math.pow(Math.sin(latDiff / 2),2)
+ Math.cos(radLat1) * Math.cos(radLat2)
* Math.pow(Math.sin(lngDiff / 2),2)
));
result = result * GpsSubscribeConst.EARTH_RADIUS;
return result;
double value = 2 * Math.asin(Math.sqrt(
Math.pow(Math.sin(latDiff / 2),2)
+ Math.cos(radLat1) * Math.cos(radLat2)
* Math.pow(Math.sin(lngDiff / 2),2)
));
return value * 6378137;
}
public void removeSubscribe(final String sid){
//========= 移除 & 清空 =========
public void removeSubscribe(String sid){
subscribeConfigMap.remove(sid);
subscriberPointMap.remove(sid);
subscriberPushCountMap.remove(sid);
}
public void clearAll(){
subscribeConfigMap.clear();
subscriberPointMap.clear();
subscriberPushCountMap.clear();
}
}

View File

@@ -4,136 +4,36 @@ import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:46
*/
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeConst;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeResult;
import cc.winboll.studio.libgpsrelaysentinel.model.LocationPoint;
/**
* 对外消息接收服务外部App可bind或start
* 收到GPS消息后通过本地广播回调给外部App
* 全局消息接收父类服务
* 所有应用内接收服务全部继承此类
*/
public final class GpsSubscribeReceiverService extends Service {
public abstract class GpsSubscribeReceiverService extends Service {
// 外部回调监听
public interface GpsMessageListener {
void onGpsLocation(LocationPoint point, GpsSubscribeMsg config);
void onSubscribeResult(GpsSubscribeResult result);
public static final String TAG_PARENT = "GpsSubscribeReceiverService";
//当前绑定的视图订阅SID
protected String bindViewSid;
public void bindControlSid(String sid){
this.bindViewSid = sid;
}
private final List<GpsMessageListener> listeners = new CopyOnWriteArrayList<GpsMessageListener>();
private final IBinder localBinder = new LocalBinder();
@Override
public void onCreate() {
super.onCreate();
/**
* 统一接收GPS推送入口
*/
public void onReceiveGpsData(LocationPoint point, GpsSubscribeMsg config){
//父类统一日志溯源
LogUtils.d(TAG_PARENT,"【消息溯源】接收视图SID" + bindViewSid);
}
// 外部App绑定服务
@Override
public IBinder onBind(Intent intent) {
return localBinder;
}
// 外部App startService 入口:接收订阅请求
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && intent.getParcelableExtra("req") != null) {
GpsSubscribeMsg msg = intent.getParcelableExtra("req");
handleSubscribeRequest(msg);
}
return START_STICKY;
}
// 处理订阅请求:发送给管理器,并回执
private void handleSubscribeRequest(final GpsSubscribeMsg msg) {
// 加入订阅管理
cc.winboll.studio.libgpsrelaysentinel.manager.GpsSubscribeManager
.getInstance().addSubscribe(msg);
cc.winboll.studio.libgpsrelaysentinel.manager.SubscribeLocationManager
.getInstance().putSubscribeConfig(msg.getSubscribeUniqueId(), msg);
// 回执成功
GpsSubscribeResult result = new GpsSubscribeResult(
msg.getSubscribeUniqueId(),
GpsSubscribeConst.RESULT_SUCCESS,
"subscribe ok",
GpsSubscribeConst.GPS_STATE_LOCATED,
1000,
System.currentTimeMillis()
);
sendSubscribeResultBroadcast(result);
notifySubscribeResult(result);
}
// 供内部GPS服务调用推送定位消息
public void pushLocation(final LocationPoint point, final GpsSubscribeMsg config) {
sendLocationBroadcast(point, config);
notifyGpsLocation(point, config);
}
// ---------- 广播回调(跨进程/外部App接收 ----------
private void sendLocationBroadcast(final LocationPoint point, final GpsSubscribeMsg config) {
Intent intent = new Intent(GpsSubscribeConst.ACTION_GPS_LOCATION);
intent.putExtra("point", point);
intent.putExtra("config", config);
sendBroadcast(intent);
}
private void sendSubscribeResultBroadcast(final GpsSubscribeResult result) {
Intent intent = new Intent(GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK);
intent.putExtra("data", result);
sendBroadcast(intent);
}
// ---------- 本地Binder同进程直接回调 ----------
public class LocalBinder extends Binder {
public GpsSubscribeReceiverService getService() {
return GpsSubscribeReceiverService.this;
}
}
public void addListener(final GpsMessageListener l) {
if (l != null && !listeners.contains(l)) {
listeners.add(l);
}
}
public void removeListener(final GpsMessageListener l) {
listeners.remove(l);
}
private void notifyGpsLocation(final LocationPoint point, final GpsSubscribeMsg config) {
for (GpsMessageListener l : listeners) {
l.onGpsLocation(point, config);
}
}
private void notifySubscribeResult(final GpsSubscribeResult result) {
for (GpsMessageListener l : listeners) {
l.onSubscribeResult(result);
}
}
@Override
public void onDestroy() {
listeners.clear();
super.onDestroy();
return null;
}
}

View File

@@ -1,174 +1,226 @@
package cc.winboll.studio.libgpsrelaysentinel.view;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:27
*/
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CompoundButton;
import android.view.LayoutInflater;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.TextView;
import cc.winboll.studio.libgpsrelaysentinel.R;
import cc.winboll.studio.libgpsrelaysentinel.manager.GpsSubscribeManager;
import cc.winboll.studio.libgpsrelaysentinel.manager.SubscribeLocationManager;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeConst;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
import cc.winboll.studio.libgpsrelaysentinel.model.LocationPoint;
import java.util.UUID;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeConst;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeResult;
import cc.winboll.studio.libgpsrelaysentinel.receiver.GpsSubscribeObserverReceiver;
import cc.winboll.studio.libgpsrelaysentinel.util.TimeCountUtil;
public final class GpsSubscribeControlView extends LinearLayout {
private RadioGroup rgSubMode;
private RadioButton rbAll;
private RadioButton rbStep;
private LinearLayout layoutStepSetting;
//常量抽取
private static final long REFRESH_INTERVAL = 600;
private RadioGroup rgSubscribeMode;
private RadioButton rbModeAll;
private RadioButton rbModeStep;
private EditText etStepMeter;
private Switch switchSubscribe;
private TextView tvSubscribeSid;
private TextView tvSubscribeRecord;
private Switch mSwitchSubscribe;
private TextView mTvCountTip;
private TimeCountUtil mTimeCountUtil;
private GpsSubscribeObserverReceiver mResultReceiver;
private String currentSubscribeSid;
private boolean isSubscribeSuccess;
//一对一专属绑定的接收服务
private Class<?> mBindReceiverServiceClazz;
//final管理器 构造器初始化
private final GpsSubscribeManager mSubscribeManager;
private final SubscribeLocationManager mLocationManager;
private final Handler mRefreshHandler = new Handler(Looper.getMainLooper());
public GpsSubscribeControlView(Context context) {
super(context);
initView();
mSubscribeManager = GpsSubscribeManager.getInstance();
mLocationManager = SubscribeLocationManager.getInstance();
initView(context);
}
public GpsSubscribeControlView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
mSubscribeManager = GpsSubscribeManager.getInstance();
mLocationManager = SubscribeLocationManager.getInstance();
initView(context);
}
private void initView(){
setOrientation(VERTICAL);
inflate(getContext(),R.layout.view_gps_subscribe_control,this);
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.view_gps_subscribe_control, this, true);
rgSubMode = findViewById(R.id.rg_sub_mode);
rbAll = findViewById(R.id.rb_all);
rbStep = findViewById(R.id.rb_step);
layoutStepSetting = findViewById(R.id.layout_step_setting);
rgSubscribeMode = findViewById(R.id.rg_subscribe_mode);
rbModeAll = findViewById(R.id.rb_mode_all);
rbModeStep = findViewById(R.id.rb_mode_step);
etStepMeter = findViewById(R.id.et_step_meter);
switchSubscribe = findViewById(R.id.switch_subscribe);
tvSubscribeSid = findViewById(R.id.tv_subscribe_sid);
tvSubscribeRecord = findViewById(R.id.tv_subscribe_record);
mSwitchSubscribe = findViewById(R.id.switch_subscribe);
mTvCountTip = findViewById(R.id.tv_count_tip);
initDefaultConfig();
initModeSwitch();
initCountUtil();
initReceiver();
initSwitchEvent();
initSubscribeSwitch();
startAutoRefreshRecord();
}
private void initModeSwitch(){
rgSubMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
private void initDefaultConfig() {
currentSubscribeSid = UUID.randomUUID().toString().substring(0, 16);
tvSubscribeSid.setText("订阅SID" + currentSubscribeSid);
rbModeAll.setChecked(true);
etStepMeter.setText("10");
}
/**
* 外部绑定当前视图专属的接收服务Class
*/
public void bindReceiverService(Class<?> serviceClazz){
this.mBindReceiverServiceClazz = serviceClazz;
}
private void initModeSwitch() {
rgSubscribeMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
layoutStepSetting.setVisibility(checkedId == R.id.rb_step ? View.VISIBLE : View.GONE);
etStepMeter.setVisibility(checkedId == R.id.rb_mode_step ? VISIBLE : GONE);
}
});
}
private void initCountUtil(){
mTimeCountUtil = new TimeCountUtil(new TimeCountUtil.OnCountListener() {
private void initSubscribeSwitch() {
switchSubscribe.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener() {
@Override
public void onTimeOut() {
if(!isSubscribeSuccess){
mSwitchSubscribe.setChecked(false);
mTvCountTip.setText("订阅超时,已自动关闭");
}
}
});
}
private void initReceiver(){
mResultReceiver = new GpsSubscribeObserverReceiver();
mResultReceiver.setOnSubscribeResultListener(new GpsSubscribeObserverReceiver.OnSubscribeResultListener() {
@Override
public void onResultBack(GpsSubscribeResult result) {
if(currentSubscribeSid.equals(result.getSubscribeUniqueId())){
isSubscribeSuccess = true;
mTimeCountUtil.cancel();
mTvCountTip.setText("订阅已生效,通讯正常");
}
}
});
IntentFilter filter = new IntentFilter();
filter.addAction(GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK);
getContext().registerReceiver(mResultReceiver,filter);
}
private void initSwitchEvent(){
mSwitchSubscribe.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if(isChecked){
public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
startSubscribe();
}else{
} else {
stopSubscribe();
}
}
});
}
private void startSubscribe(){
isSubscribeSuccess = false;
currentSubscribeSid = UUID.randomUUID().toString();
mTvCountTip.setText("等待订阅返回中...");
private void startSubscribe() {
int subMode = GpsSubscribeConst.SUB_TYPE_ALL;
float stepMeter = 10.0f;
float stepVal = 10f;
if(rbStep.isChecked()){
if (rbModeStep.isChecked()) {
subMode = GpsSubscribeConst.SUB_TYPE_STEP_DISTANCE;
try{
stepMeter = Float.parseFloat(etStepMeter.getText().toString().trim());
}catch (Exception e){
stepMeter = 10.0f;
}
try {
stepVal = Float.parseFloat(etStepMeter.getText().toString().trim());
} catch (Exception ignored) {}
}
GpsSubscribeMsg msg = new GpsSubscribeMsg(
GpsSubscribeMsg subscribeMsg = new GpsSubscribeMsg(
getContext().getPackageName(),
subMode,
stepMeter,
stepVal,
GpsSubscribeConst.SUBSCRIBE_TYPE_LOCATION,
1000,
1.0f,
1f,
true,
currentSubscribeSid
);
Intent intent = new Intent(GpsSubscribeConst.ACTION_SUBSCRIBE_REQUEST);
intent.putExtra("req",msg);
getContext().sendBroadcast(intent);
mSubscribeManager.addSubscribe(subscribeMsg);
mLocationManager.putSubscribeConfig(currentSubscribeSid, subscribeMsg);
mLocationManager.clearPushCount(currentSubscribeSid);
mTimeCountUtil.start(GpsSubscribeConst.SUBSCRIBE_TIME_OUT);
}
private void stopSubscribe(){
mTimeCountUtil.cancel();
isSubscribeSuccess = false;
mTvCountTip.setText("订阅已关闭");
}
public void release(){
mTimeCountUtil.cancel();
if(mResultReceiver != null){
getContext().unregisterReceiver(mResultReceiver);
//开启订阅自动启动专属接收服务
if(mBindReceiverServiceClazz != null){
Intent startServiceIntent = new Intent(getContext(), mBindReceiverServiceClazz);
getContext().startService(startServiceIntent);
}
}
private void stopSubscribe() {
mSubscribeManager.removeSubscribe(currentSubscribeSid);
mLocationManager.removeSubscribe(currentSubscribeSid);
tvSubscribeRecord.setText("状态:未订阅");
//关闭订阅 同步停止专属接收服务
if(mBindReceiverServiceClazz != null){
Intent stopServiceIntent = new Intent(getContext(), mBindReceiverServiceClazz);
getContext().stopService(stopServiceIntent);
}
}
private void startAutoRefreshRecord() {
mRefreshHandler.postDelayed(new Runnable() {
@Override
public void run() {
refreshRecordInfo();
mRefreshHandler.postDelayed(this, REFRESH_INTERVAL);
}
}, REFRESH_INTERVAL);
}
private void refreshRecordInfo() {
if (!switchSubscribe.isChecked()) {
tvSubscribeRecord.setText("状态:空闲未订阅");
return;
}
GpsSubscribeMsg config = mLocationManager.getSubscribeConfig(currentSubscribeSid);
LocationPoint lastPoint = mLocationManager.getLastPoint(currentSubscribeSid);
if (config == null) {
tvSubscribeRecord.setText("状态:已订阅|等待管理器加载");
return;
}
String modeText = config.getSubscribeMode() == GpsSubscribeConst.SUB_TYPE_ALL
? "全量订阅" : "步长订阅";
int realPushCount = mLocationManager.getPushCount(currentSubscribeSid);
StringBuilder record = new StringBuilder();
record.append("【订阅实时数据表】\n");
record.append("订阅模式:").append(modeText).append("\n");
record.append("步长阈值:").append(config.getStepDistanceM()).append("\n");
if(lastPoint != null){
record.append("基准定点:").append(lastPoint.getLatitude()).append(" , ").append(lastPoint.getLongitude()).append("\n");
}else{
record.append("基准定点:等待首次定位建立\n");
}
record.append("真实推送次数:").append(realPushCount).append("");
tvSubscribeRecord.setText(record);
}
public String getCurrentSid() {
return currentSubscribeSid;
}
public boolean isSubscribeOpen() {
return switchSubscribe.isChecked();
}
/**
* 视图销毁:强制停止订阅 + 停止服务 + 清空刷新任务
*/
@Override
protected void onDetachedFromWindow() {
if(switchSubscribe.isChecked()){
switchSubscribe.setChecked(false);
}
mRefreshHandler.removeCallbacksAndMessages(null);
super.onDetachedFromWindow();
}
}

View File

@@ -3,97 +3,81 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="订阅模式:"
android:textSize="14sp"/>
<RadioGroup
android:id="@+id/rg_sub_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="全消息订阅"
android:checked="true"/>
<RadioButton
android:id="@+id/rb_step"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="步长订阅"/>
</RadioGroup>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_step_setting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="移动步长阈值(米)"
android:textSize="14sp"/>
<EditText
android:id="@+id/et_step_meter"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="10"
android:gravity="center"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GPS订阅总开关"
android:textSize="14sp"/>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<Switch
android:id="@+id/switch_subscribe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
android:padding="14dp"
android:layout_marginVertical="6dp"
android:background="#282828"
android:clipToPadding="false">
<!-- SID标识 -->
<TextView
android:id="@+id/tv_count_tip"
android:id="@+id/tv_subscribe_sid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#999999"
android:textSize="11sp"/>
<!-- 订阅模式选择 -->
<RadioGroup
android:id="@+id/rg_subscribe_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="未订阅"
android:textSize="12sp"
android:textColor="#666666"/>
android:orientation="horizontal"
android:layout_marginTop="8dp">
<RadioButton
android:id="@+id/rb_mode_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="全量订阅"
android:textColor="#FFFFFF"/>
<RadioButton
android:id="@+id/rb_mode_step"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="步长订阅"
android:layout_marginStart="18dp"
android:textColor="#FFFFFF"/>
</RadioGroup>
<!-- 步长输入 -->
<EditText
android:id="@+id/et_step_meter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="移动阈值(米)"
android:inputType="numberDecimal"
android:text="10"
android:visibility="gone"
android:textColor="#ffffff"
android:textColorHint="#666666"
android:layout_marginTop="8dp"/>
<!-- 订阅开关 -->
<Switch
android:id="@+id/switch_subscribe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开启独立订阅"
android:textColor="#EEEEEE"
android:layout_marginTop="10dp"/>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#444444"
android:layout_marginTop="12dp"/>
<!-- 订阅数据记录表【ID完全对应源码 tv_subscribe_record】 -->
<TextView
android:id="@+id/tv_subscribe_record"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#88EE88"
android:textSize="10sp"
android:layout_marginTop="10dp"
android:gravity="start"/>
</LinearLayout>