From b846d352a239842660a0bd3efde80fbb381086ff Mon Sep 17 00:00:00 2001 From: userGyl Date: Tue, 10 Mar 2026 17:31:27 +0800 Subject: [PATCH] =?UTF-8?q?android=20=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/android/app/build.gradle.kts | 5 + .../android/app/src/main/AndroidManifest.xml | 8 + .../java/com/lnkj/ln_jq_app/MainActivity.java | 189 ++++++++++++++++++ .../com/lnkj/ln_jq_app/NativeMapFactory.java | 37 ++++ .../com/lnkj/ln_jq_app/NativeMapView.java | 143 +++++++++++++ .../app/src/main/res/drawable/amap_loc.xml | 10 + .../src/main/res/drawable/ic_end_marker.xml | 10 + .../app/src/main/res/drawable/ic_location.xml | 10 + .../app/src/main/res/drawable/ic_route.xml | 13 ++ .../src/main/res/drawable/ic_start_marker.xml | 10 + .../main/res/drawable/ic_station_marker.xml | 10 + .../c_page/base_widgets/NativePageIOS.dart | 116 ++++++----- 12 files changed, 515 insertions(+), 46 deletions(-) create mode 100644 ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapFactory.java create mode 100644 ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapView.java create mode 100644 ln_jq_app/android/app/src/main/res/drawable/amap_loc.xml create mode 100644 ln_jq_app/android/app/src/main/res/drawable/ic_end_marker.xml create mode 100644 ln_jq_app/android/app/src/main/res/drawable/ic_location.xml create mode 100644 ln_jq_app/android/app/src/main/res/drawable/ic_route.xml create mode 100644 ln_jq_app/android/app/src/main/res/drawable/ic_start_marker.xml create mode 100644 ln_jq_app/android/app/src/main/res/drawable/ic_station_marker.xml diff --git a/ln_jq_app/android/app/build.gradle.kts b/ln_jq_app/android/app/build.gradle.kts index 2539bdf..24d95b7 100644 --- a/ln_jq_app/android/app/build.gradle.kts +++ b/ln_jq_app/android/app/build.gradle.kts @@ -68,3 +68,8 @@ android { flutter { source = "../.." } + +dependencies { + implementation("com.amap.api:navi-3dmap-location-search:10.0.700_3dmap10.0.700_loc6.4.5_sea9.7.2") + implementation("com.squareup.okhttp3:okhttp:4.12.0") +} diff --git a/ln_jq_app/android/app/src/main/AndroidManifest.xml b/ln_jq_app/android/app/src/main/AndroidManifest.xml index 5392736..7a6ef64 100644 --- a/ln_jq_app/android/app/src/main/AndroidManifest.xml +++ b/ln_jq_app/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,14 @@ android:label="小羚羚" android:name="${applicationName}" android:icon="@mipmap/logo"> + + + + + + { + switch (call.method) { + case "requestPermissions": + requestPermissions(); + result.success(null); + break; + case "onResume": + if (mapView != null) { + mapView.onResume(); + } + result.success(null); + break; + case "onPause": + if (mapView != null) { + mapView.onPause(); + } + result.success(null); + break; + case "onDestroy": + if (mapView != null) { + mapView.dispose(); + mapView = null; + } + result.success(null); + break; + default: + result.notImplemented(); + break; + } + }); + } + + /** + * 获取当前系统版本需要申请的权限列表 + */ + private String[] getRequiredPermissions() { + List permissions = new ArrayList<>(); + // 定位权限是必须的 + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION); + permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION); + + // 存储权限处理:Android 13 (API 33) 以下才需要申请 legacy 存储权限 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE); + } + + return permissions.toArray(new String[0]); + } + + /** + * 检查并申请权限 + */ + private void checkAndRequestPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + String[] requiredPermissions = getRequiredPermissions(); + List deniedPermissions = new ArrayList<>(); + + for (String permission : requiredPermissions) { + if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + deniedPermissions.add(permission); + } + } + + if (!deniedPermissions.isEmpty()) { + ActivityCompat.requestPermissions( + this, + deniedPermissions.toArray(new String[0]), + PERMISSION_REQUEST_CODE + ); + } else { + Log.d(TAG, "所有必要权限已授予"); + if (mapView != null) { + mapView.startLocation(); + } + } + } else { + if (mapView != null) { + mapView.startLocation(); + } + } + } + + private void requestPermissions() { + checkAndRequestPermissions(); + } + + public void setMapView(NativeMapView mapView) { + this.mapView = mapView; + } + + @Override + protected void onResume() { + super.onResume(); + // 注意:高德SDK合规检查通过后再进行定位相关操作 + // 这里仅保留地图生命周期调用,权限建议在Flutter端或按需触发 + if (mapView != null) { + mapView.onResume(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (mapView != null) { + mapView.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mapView != null) { + mapView.dispose(); + mapView = null; + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (mapView != null) { + mapView.onSaveInstanceState(outState); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode == PERMISSION_REQUEST_CODE) { + boolean locationGranted = false; + for (int i = 0; i < permissions.length; i++) { + if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[i]) + && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + locationGranted = true; + break; + } + } + + if (locationGranted) { + if (mapView != null) { + mapView.startLocation(); + } + } else { + // 只有在定位权限确实被拒绝时才弹出提示 + Toast.makeText(this, "请授予应用定位权限以正常使用地图功能", Toast.LENGTH_LONG).show(); + } + } + } } diff --git a/ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapFactory.java b/ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapFactory.java new file mode 100644 index 0000000..9e8ff31 --- /dev/null +++ b/ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapFactory.java @@ -0,0 +1,37 @@ +package com.lnkj.ln_jq_app; + +import android.content.Context; + +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; + +/** + * 高德地图导航 Platform View Factory + * 对应iOS的NativeViewFactory + */ +public class NativeMapFactory extends PlatformViewFactory { + + private static final String VIEW_TYPE_ID = "NativeFirstPage"; + private static NativeMapView mapViewInstance = null; + private final Context context; + + public NativeMapFactory(Context context) { + super(StandardMessageCodec.INSTANCE); + this.context = context; + } + + @Override + public PlatformView create(Context context, int viewId, Object args) { + mapViewInstance = new NativeMapView(context, viewId, args); + return mapViewInstance; + } + + /** + * 获取地图实例,供MainActivity使用 + */ + public static NativeMapView getMapView() { + return mapViewInstance; + } +} diff --git a/ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapView.java b/ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapView.java new file mode 100644 index 0000000..63cece6 --- /dev/null +++ b/ln_jq_app/android/app/src/main/java/com/lnkj/ln_jq_app/NativeMapView.java @@ -0,0 +1,143 @@ +package com.lnkj.ln_jq_app; + +import android.content.Context; +import android.graphics.Color; +import android.os.Bundle; +import android.util.Log; +import android.view.View; + +import com.amap.api.location.AMapLocation; +import com.amap.api.location.AMapLocationClient; +import com.amap.api.location.AMapLocationClientOption; +import com.amap.api.location.AMapLocationListener; +import com.amap.api.maps.AMap; +import com.amap.api.maps.LocationSource; +import com.amap.api.maps.MapView; +import com.amap.api.maps.MapsInitializer; +import com.amap.api.maps.model.MyLocationStyle; + +import io.flutter.plugin.platform.PlatformView; + +public class NativeMapView implements PlatformView, LocationSource, AMapLocationListener { + private static final String TAG = "NativeMapView"; + private final MapView mapView; + private final AMap aMap; + private OnLocationChangedListener mListener; + private AMapLocationClient mlocationClient; + private final Context mContext; + + public NativeMapView(Context context, int id, Object args) { + this.mContext = context; + + // 隐私合规接口,必须在创建MapView之前设置 + MapsInitializer.updatePrivacyShow(context, true, true); + MapsInitializer.updatePrivacyAgree(context, true); + + mapView = new MapView(context); + // 在 Flutter PlatformView 中,savedInstanceState 通常为 null + mapView.onCreate(null); + aMap = mapView.getMap(); + + // 设置地图的 UI 选项 + setupMapUi(); + + // 注册到 MainActivity 以便同步生命周期 + if (context instanceof MainActivity) { + ((MainActivity) context).setMapView(this); + } + } + + private void setupMapUi() { + aMap.setLocationSource(this); + aMap.setMyLocationEnabled(true); + + MyLocationStyle myLocationStyle = new MyLocationStyle(); + myLocationStyle.interval(2000); + // 连续定位并将视角移动到地图中心点,定位蓝点跟随设备移动 + myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATE); + myLocationStyle.showMyLocation(true); + myLocationStyle.strokeColor(Color.TRANSPARENT); + myLocationStyle.radiusFillColor(Color.TRANSPARENT); + aMap.setMyLocationStyle(myLocationStyle); + + // 显示缩放按钮 + aMap.getUiSettings().setZoomControlsEnabled(true); + } + + @Override + public View getView() { + return mapView; + } + + @Override + public void activate(OnLocationChangedListener listener) { + mListener = listener; + startLocation(); + } + + @Override + public void deactivate() { + mListener = null; + if (mlocationClient != null) { + mlocationClient.stopLocation(); + mlocationClient.onDestroy(); + } + mlocationClient = null; + } + + public void startLocation() { + if (mlocationClient == null) { + try { + AMapLocationClient.updatePrivacyShow(mContext, true, true); + AMapLocationClient.updatePrivacyAgree(mContext, true); + mlocationClient = new AMapLocationClient(mContext); + AMapLocationClientOption mLocationOption = new AMapLocationClientOption(); + mlocationClient.setLocationListener(this); + mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); + mlocationClient.setLocationOption(mLocationOption); + mlocationClient.startLocation(); + } catch (Exception e) { + Log.e(TAG, "startLocation error", e); + } + } + } + + @Override + public void onLocationChanged(AMapLocation amapLocation) { + if (mListener != null && amapLocation != null) { + if (amapLocation.getErrorCode() == 0) { + mListener.onLocationChanged(amapLocation); + // 首次定位成功后缩放地图 + // aMap.moveCamera(CameraUpdateFactory.zoomTo(15)); + } else { + Log.e(TAG, "定位失败," + amapLocation.getErrorCode() + ": " + amapLocation.getErrorInfo()); + } + } + } + + public void onResume() { + if (mapView != null) { + mapView.onResume(); + } + } + + public void onPause() { + if (mapView != null) { + mapView.onPause(); + } + } + + public void onSaveInstanceState(Bundle outState) { + if (mapView != null) { + mapView.onSaveInstanceState(outState); + } + } + + @Override + public void dispose() { + if (mapView != null) { + mapView.onDestroy(); + } + deactivate(); + } +} diff --git a/ln_jq_app/android/app/src/main/res/drawable/amap_loc.xml b/ln_jq_app/android/app/src/main/res/drawable/amap_loc.xml new file mode 100644 index 0000000..a58e47e --- /dev/null +++ b/ln_jq_app/android/app/src/main/res/drawable/amap_loc.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ln_jq_app/android/app/src/main/res/drawable/ic_end_marker.xml b/ln_jq_app/android/app/src/main/res/drawable/ic_end_marker.xml new file mode 100644 index 0000000..58bb442 --- /dev/null +++ b/ln_jq_app/android/app/src/main/res/drawable/ic_end_marker.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ln_jq_app/android/app/src/main/res/drawable/ic_location.xml b/ln_jq_app/android/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 0000000..4e99139 --- /dev/null +++ b/ln_jq_app/android/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ln_jq_app/android/app/src/main/res/drawable/ic_route.xml b/ln_jq_app/android/app/src/main/res/drawable/ic_route.xml new file mode 100644 index 0000000..e16e2fe --- /dev/null +++ b/ln_jq_app/android/app/src/main/res/drawable/ic_route.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/ln_jq_app/android/app/src/main/res/drawable/ic_start_marker.xml b/ln_jq_app/android/app/src/main/res/drawable/ic_start_marker.xml new file mode 100644 index 0000000..0af42cf --- /dev/null +++ b/ln_jq_app/android/app/src/main/res/drawable/ic_start_marker.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ln_jq_app/android/app/src/main/res/drawable/ic_station_marker.xml b/ln_jq_app/android/app/src/main/res/drawable/ic_station_marker.xml new file mode 100644 index 0000000..ef73731 --- /dev/null +++ b/ln_jq_app/android/app/src/main/res/drawable/ic_station_marker.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ln_jq_app/lib/pages/c_page/base_widgets/NativePageIOS.dart b/ln_jq_app/lib/pages/c_page/base_widgets/NativePageIOS.dart index a21b0cc..150ab11 100644 --- a/ln_jq_app/lib/pages/c_page/base_widgets/NativePageIOS.dart +++ b/ln_jq_app/lib/pages/c_page/base_widgets/NativePageIOS.dart @@ -1,58 +1,82 @@ +import 'dart:io'; + import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'dart:io'; - import 'package:flutter/services.dart'; +/// 原生地图页面 class NativePageIOS extends StatelessWidget { - const NativePageIOS({super.key}); + const NativePageIOS({super.key}); - @override - Widget build(BuildContext context) { - if (Platform.isIOS) { - return Container( - // onTap: () => _handleTap(context), - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.width - 100, - // padding: EdgeInsetsGeometry.all(15), - color: Colors.red[100], - - child: UiKitView( - viewType: 'NativeFirstPage', // 与原生端注册的标识一致 - gestureRecognizers: >{}.toSet(), - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - creationParamsCodec: const StandardMessageCodec(), - layoutDirection: TextDirection.ltr, - ) - ); - } else { - return const Center(child: Text('not ios')); - } - } - - - void _handleTap(BuildContext context) { - print("页面被点击"); - _showDialog(context, 'Tip', '点击了'); - } - - - void _showDialog(BuildContext context, String title, String content) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(content), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('确定'), - ), - ], - ), + @override + Widget build(BuildContext context) { + if (Platform.isIOS) { + return _buildIOSView(context); + } else if (Platform.isAndroid) { + return _buildAndroidView(context); + } else { + return const Center( + child: Text('不支持的平台', style: TextStyle(fontSize: 16)), ); } + } + /// 构建iOS Platform View + Widget _buildIOSView(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height - 100, + color: Colors.white, + child: UiKitView( + viewType: 'NativeFirstPage', // 与iOS原生端注册的标识一致 + gestureRecognizers: >{}.toSet(), + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + creationParamsCodec: const StandardMessageCodec(), + layoutDirection: TextDirection.ltr, + ), + ); + } + + /// 构建Android Platform View + Widget _buildAndroidView(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height - 100, + color: Colors.white, + child: AndroidView( + viewType: 'NativeFirstPage', // 与Android原生端注册的标识一致 + gestureRecognizers: >{}.toSet(), + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + creationParamsCodec: const StandardMessageCodec(), + layoutDirection: TextDirection.ltr, + ), + ); + } + + /// 处理点击事件(如需要) + void _handleTap(BuildContext context) { + if (kDebugMode) { + print("NativePage被点击"); + } + _showDialog(context, '提示', '点击了原生地图页面'); + } + + /// 显示对话框 + void _showDialog(BuildContext context, String title, String content) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(content), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('确定'), + ), + ], + ), + ); + } } \ No newline at end of file