From e24c9bdce36e60ef8d121c4765e59577373bfc78 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Thu, 7 May 2026 14:37:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9BGPS=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=8F=91=E9=80=81=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/SubscribeLocationManager.java | 81 ++++-- .../service/GpsSubscribeReceiverService.java | 136 ++------- .../view/GpsSubscribeControlView.java | 266 +++++++++++------- .../res/layout/view_gps_subscribe_control.xml | 158 +++++------ 4 files changed, 301 insertions(+), 340 deletions(-) diff --git a/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/manager/SubscribeLocationManager.java b/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/manager/SubscribeLocationManager.java index 9c3548b..962086f 100644 --- a/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/manager/SubscribeLocationManager.java +++ b/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/manager/SubscribeLocationManager.java @@ -1,10 +1,5 @@ package cc.winboll.studio.libgpsrelaysentinel.manager; -/** - * @Author 豆包&ZhanGSKen - * @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 subscribeConfigMap; + //基准定点坐标 private final Map subscriberPointMap; + //真实推送计数(精准统计) + private final Map subscriberPushCountMap; private SubscribeLocationManager(){ subscribeConfigMap = new HashMap(); subscriberPointMap = new HashMap(); + subscriberPushCountMap = new HashMap(); } 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(); } } diff --git a/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/service/GpsSubscribeReceiverService.java b/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/service/GpsSubscribeReceiverService.java index ce2c659..0dcfec9 100644 --- a/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/service/GpsSubscribeReceiverService.java +++ b/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/service/GpsSubscribeReceiverService.java @@ -4,136 +4,36 @@ import android.app.Service; import android.content.Intent; import android.os.IBinder; -/** - * @Author 豆包&ZhanGSKen - * @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 listeners = new CopyOnWriteArrayList(); - 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; } } diff --git a/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/view/GpsSubscribeControlView.java b/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/view/GpsSubscribeControlView.java index 08b0e95..732e614 100644 --- a/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/view/GpsSubscribeControlView.java +++ b/libgpsrelaysentinel/src/main/java/cc/winboll/studio/libgpsrelaysentinel/view/GpsSubscribeControlView.java @@ -1,174 +1,226 @@ package cc.winboll.studio.libgpsrelaysentinel.view; -/** - * @Author 豆包&ZhanGSKen - * @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(); + } } diff --git a/libgpsrelaysentinel/src/main/res/layout/view_gps_subscribe_control.xml b/libgpsrelaysentinel/src/main/res/layout/view_gps_subscribe_control.xml index e35e2ad..86f3437 100644 --- a/libgpsrelaysentinel/src/main/res/layout/view_gps_subscribe_control.xml +++ b/libgpsrelaysentinel/src/main/res/layout/view_gps_subscribe_control.xml @@ -3,97 +3,81 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="16dp"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:padding="14dp" + android:layout_marginVertical="6dp" + android:background="#282828" + android:clipToPadding="false"> + + + + + android:orientation="horizontal" + android:layout_marginTop="8dp"> + + + + + + + + + + + + + + + + +