前言
这个腾讯地图的demo可以做到:1.定位位置2.规划起点和终点的路线3.显示规划路线距离以及计算路费4.动画模拟小车在规划路线上的平滑移动。
实现步骤
先看下实现效果:新建个Android项目并新建个Activity,命名为DrivingRouteActivity,先来画一下UI布局,布局比较简单,由一个腾讯SDK包下的地图组件MapView,以及两个用于输入起始位置的输入框,两个确认路线规划的Button,一个定位当前位置的ImageView,一个用于显示行程信息的TextView组成,布局代码只是为了方便展示实现功能,所以下面直接贴出布局代码:
1.账号注册与配置
在开发之前,我们需要到腾讯位置服务官网注册一个账号
注册后进入控制台
将上面申请的key在application标签下进行如下配置(value替换成自己的key)
2.引入腾讯Android地图SDK
进入Android地图SDK,下载3D版地图SDK压缩包
下载完成后打开压缩包,将libs文件夹下的jar包拷贝到app的libs目录下,右键该jar包选择add as library添加为依赖,并且在项目app\src\main路径下建立名为jniLibs的目录,把压缩包libs/jniLibs/strip文件夹下的所有包放到jniLibs目录下
3.地图初始化
配置完成,现在开始实现我们的逻辑交互,为了让实现逻辑更加清晰,我将业务逻辑代码与视图渲染代码分到了两个包中,除了activity包中的DrivingRouteActivity之外,新建了一个present包,并在包下建立一个DrivingRoutePresent类,分别由DrivingRouteActivity负责对UI组件进行视图渲染,由DrivingRoutePresent类负责业务逻辑。这里我还新建了一个contract包,并创建一个DrivingRouteContract接口,通过这个接口定义的方法,实现DrivingRoutePresent与DrivingRouteActivity之间的交互。我们在DrivingRouteContract接口中定义两个接口,一个View接口供DrivingRouteActivity实现,一个Presenter接口供DrivingRoutePresent实现,并定义一些初始化的方法
public interface DrivingRouteContract {
interface View{
void initView();//初始化View
void initOnClick();//初始化OnClickListener
void setOrderDescTV(String content);//渲染订单行程信息
EditText getFromET();
}
interface Presenter{
void attachView(DrivingRouteContract.View view);//绑定View
}
}
接着再让DrivingRouteActivity实现DrivingRouteContract.View接口并声明UI中的组件进行初始化
public class DrivingRouteActivity extends Activity implements DrivingRouteContract.View, View.OnClickListener {
private MapView mapView;
private TencentMap mMap;
private Button confirmBtn;
private Button orderBtn;
private ImageButton locationIB;
private EditText fromET;
private EditText toET;
private TextView orderDescTV;
private DrivingRoutePresent drivingRoutePresent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_driving_route);
initView();
initOnClick();
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.order_btn:
//实现行程路线规划
break;
case R.id.confirm_btn:
//开启动画移动
break;
case R.id.location_ib:
//定位当前位置
break;
}
}
/**
* mapview的生命周期管理
*/
@Override
protected void onStart() {
super.onStart();
mapView.onStart();
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
protected void onStop() {
super.onStop();
mapView.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
@Override
protected void onRestart() {
super.onRestart();
mapView.onRestart();
}
@Override
public void initView() {
mapView = findViewById(R.id.mapview);
confirmBtn = findViewById(R.id.confirm_btn);
orderBtn = findViewById(R.id.order_btn);
locationIB = findViewById(R.id.location_ib);
fromET = findViewById(R.id.from_et);
toET = findViewById(R.id.to_et);
orderDescTV = findViewById(R.id.orderdesc_tv);
mMap = mapView.getMap();
drivingRoutePresent = new DrivingRoutePresent();
drivingRoutePresent.attachView(this);
}
@Override
public void initOnClick() {
orderBtn.setOnClickListener(this);
confirmBtn.setOnClickListener(this);
locationIB.setOnClickListener(this);
}
@Override
public void setOrderDescTV(String content) {
orderDescTV.setText(content);
}
@Override
public EditText getFromET() {
return fromET;
}
}
DrivingRoutePresent实现DrivingRouteContract.Presenter接口
public class DrivingRoutePresent implements DrivingRouteContract.Presenter {
private DrivingRouteContract.View drinvingRouteView;
@Override
public void attachView(DrivingRouteContract.View view) {
drinvingRouteView = view;
}
}
因为我们后面在多个地方都需要用到当前应用的上下文,为了方便,需要再编写一个全局的应用上下文工具类来帮助我们获取上下文,建立一个util包并创建一个GlobalApplication类
public class GlobalApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getContext(){
return context;
}
}
同时,在Android类文件的application标签中加入下面属性,让应用启动时加载上面的GlobalApplication
android:name=".util.GlobalApplication"
到这里,我们就完成了界面与业务代码的基本设计,运行app,可以看到显示的基本地图信息。接下来我们来实现一下路线规划的功能。腾讯官方Android地图SDK开发文档对路线规划服务和地址解析都有较详细的说明。
另外还提供了调用示例Demo。如果不清楚如何调用的话可以参考官方Demo或参考下面代码。
4.地址解析与路线规划
首先我们在DrivingRouteContract.Presenter接口申明一个用于通过地址查找经纬度的geocoder方法和一个用于路线规划的routePlan方法
public interface DrivingRouteContract {
interface View{
void initView();//初始化View
void initOnClick();//初始化OnClickListener
void setOrderDescTV(String content);//渲染订单行程信息
EditText getFromET();
}
interface Presenter{
void attachView(DrivingRouteContract.View view);//绑定View
void geocoder(String address, Integer type);//地址解码,转经纬度
void routePlan();//实现路线规划
}
}
通过腾讯Android地图SDK路线规划服务的开发文档,我们了解到要获得规划路线需要先获取起点和终点的经纬度,而在一般业务场景中,我们几乎不会让用户手动输入经纬度,所以我这里还需要用到地址解析服务,通过输入中文地址来获取经纬度,再通过经纬度规划路线(不过在实际业务中最好是加上关键词输入提示这个服务,方便用户找到输入的位置)。
在DrivingRoutePresent类中实现这两个方法
public static final Integer FROM_TYPE = 0x100; //获取起始位置坐标
public static final Integer TO_TYPE = 0x101; //获取目的位置坐标
private LatLng fromLatLng;
private LatLng toLatLng;
/**
* 地址解码
* @param address 传入需要解码的地址
* @param type 地址类型,起始位置、目的位置
*/
@Override
public void geocoder(String address, final Integer type) {
TencentSearch tencentSearch = new TencentSearch(GlobalApplication.getContext());
Address2GeoParam address2GeoParam =
new Address2GeoParam(address);
tencentSearch.address2geo(address2GeoParam, new HttpResponseListener() {
@Override
public void onSuccess(int arg0, BaseObject arg1) {
if (arg1 == null) {
return;
}
Address2GeoResultObject obj = (Address2GeoResultObject)arg1;
if (obj.result.latLng != null) {
if (type==FROM_TYPE)
fromLatLng = obj.result.latLng;
else if (type==TO_TYPE)
toLatLng = obj.result.latLng;
routePlan();
}
}
@Override
public void onFailure(int arg0, String arg1, Throwable arg2) {
Log.e("test", "error code:" + arg0 + ", msg:" + arg1);
}
});
}
private TencentSearch tencentSearch = new TencentSearch(GlobalApplication.getContext());
private StringBuffer lineStringBuilder = new StringBuffer();//路线坐标
private Double taxiFare = 0d;//预估打车费用
private Float distance = 0f;//预计全程里程
/**
* 路线规划
*/
@Override
public void routePlan() {
if (fromLatLng!=null&&toLatLng!=null){
Toast.makeText(GlobalApplication.getContext(), "正在为您规划路线", Toast.LENGTH_SHORT).show();
DrivingParam drivingParam = new DrivingParam(fromLatLng, toLatLng);
drivingParam.policy(DrivingParam.Policy.TRIP);//驾车路线规划策略,网约车场景,送乘客
drivingParam.setCarNumber("粤A00001");//填入车牌号,在路线规划时会避让车牌限行区域
tencentSearch.getRoutePlan(drivingParam, new HttpResponseListener() {
@Override
public void onSuccess(int i, DrivingResultObject drivingResultObject) {
for (DrivingResultObject.Route route : drivingResultObject.result.routes){
for (LatLng latLng : route.polyline){
lineStringBuilder.append(latLng.latitude + "," + latLng.longitude);
lineStringBuilder.append(",");
}
distance += route.distance;
taxiFare += route.taxi_fare.fare;
}
drinvingRouteView.setOrderDescTV("行程大约" + distance + "m,预计¥" + taxiFare + "元");
//清空行程路线,里程,费用信息
lineStringBuilder = new StringBuffer();
distance = 0f;
taxiFare = 0d;
}
@Override
public void onFailure(int i, String s, Throwable throwable) {
Log.d("DrivingRouteActivity", "onSuccess: " + s + i);
}
});
fromLatLng=null;
toLatLng=null;
}
}
其中geocoder方法用于获得我们输入的起始位置(从哪儿上车),以及输入的目的位置(到哪儿下车)的坐标经纬度,记录位置的经纬度后调用routePlan方法请求路线规划接口,并记录下里程,费用信息,路线行驶过程中经过的点的经纬度(用于后面实现小车移动)。
路线规划接口除了上面使用的几个常用参数外,还有很多接口参数,具体可以查看官方接口文档按需要加入
参考官方接口文档:https://lbs.qq.com/AndroidDocs/doc_3d/index.html
5.车辆行驶动画
有了路线规划方法后,给"预约快车"按钮添加实现
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.order_btn:
drivingRoutePresent.geocoder(fromET.getText().toString(), DrivingRoutePresent.FROM_TYPE);
drivingRoutePresent.geocoder(toET.getText().toString(), DrivingRoutePresent.TO_TYPE);
confirmBtn.setVisibility(View.VISIBLE);
orderBtn.setVisibility(View.GONE);
break;
case R.id.confirm_btn:
//开启动画移动
break;
case R.id.location_ib:
//定位当前位置
break;
}
}
此时,运行APP,输入起点和终点就可以获得驾车的规划路线了
接下来,我们再实现一下效果图上小车根据规划路线行驶的功能
在DrivingRouteContract.View接口中加入小车动画初始化方法initAnimation
public interface DrivingRouteContract {
interface Model{
}
interface View{
void initView();//初始化View
void initOnClick();//初始化OnClickListener
void setOrderDescTV(String content);//渲染订单行程信息
EditText getFromET();
void initAnimation(String line);//初始化小车移动动画
}
interface Presenter{
void attachView(DrivingRouteContract.View view);//绑定View
void startLocation(boolean single);
void stopLocation();
void geocoder(String address, Integer type);//地址解码,转经纬度
void routePlan();//实现路线规划
}
}
实现initAnimation方法,关于Marker其他参数同样参考上面的接口文档
private Marker mCarMarker;
private LatLng[] mCarLatLngArray;
private MarkerTranslateAnimator mAnimator;
@Override
public void initAnimation(String line) {
//拆分获得经纬度数组
String[] linePointsStr = line.split(",");
mCarLatLngArray = new LatLng[linePointsStr.length / 2];
for (int i = 0; i < mCarLatLngArray.length; i++) {
double latitude = Double.parseDouble(linePointsStr[i * 2]);
double longitude = Double.parseDouble(linePointsStr[i * 2 + 1]);
mCarLatLngArray[i] = new LatLng(latitude, longitude);
}
//添加小车路线
mMap.addPolyline(new PolylineOptions().add(mCarLatLngArray)
.color(R.color.colorLine));//这个颜色是colors.xml中自定义的颜色
//添加小车
LatLng carLatLng = mCarLatLngArray[0];
mCarMarker = mMap.addMarker(
new MarkerOptions(carLatLng)
.anchor(0.5f, 0.5f)
.icon(BitmapDescriptorFactory.fromResource(R.mipmap.taxi_t))//小车图标
.flat(true)
.clockwise(false));
//创建移动动画
mAnimator = new MarkerTranslateAnimator(mCarMarker, 50 * 1000, mCarLatLngArray, true);
//调整最佳视野
mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(
LatLngBounds.builder().include(Arrays.asList(mCarLatLngArray)).build(), 50));
}
并在routePlan方法中调用这个方法,传入行驶路线字符串
//初始化小车动画
drinvingRouteView.initAnimation(lineStringBuilder.substring(0, lineStringBuilder.length()-1));
完整代码参考
/**
* 路线规划
*/
@Override
public void routePlan() {
if (fromLatLng!=null&&toLatLng!=null){
Toast.makeText(GlobalApplication.getContext(), "正在为您规划路线", Toast.LENGTH_SHORT).show();
DrivingParam drivingParam = new DrivingParam(fromLatLng, toLatLng);
drivingParam.policy(DrivingParam.Policy.TRIP);//驾车路线规划策略,网约车场景,送乘客
drivingParam.setCarNumber("粤A00001");//填入车牌号,在路线规划时会避让车牌限行区域
tencentSearch.getRoutePlan(drivingParam, new HttpResponseListener() {
@Override
public void onSuccess(int i, DrivingResultObject drivingResultObject) {
for (DrivingResultObject.Route route : drivingResultObject.result.routes){
for (LatLng latLng : route.polyline){
lineStringBuilder.append(latLng.latitude + "," + latLng.longitude);
lineStringBuilder.append(",");
}
distance += route.distance;
taxiFare += route.taxi_fare.fare;
}
//初始化小车动画
drinvingRouteView.initAnimation(lineStringBuilder.substring(0, lineStringBuilder.length()-1));
drinvingRouteView.setOrderDescTV("行程大约" + distance + "m,预计¥" + taxiFare + "元");
//清空行程路线,里程,费用信息
lineStringBuilder = new StringBuffer();
distance = 0f;
taxiFare = 0d;
}
@Override
public void onFailure(int i, String s, Throwable throwable) {
Log.d("DrivingRouteActivity", "onSuccess: " + s + i);
}
});
fromLatLng=null;
toLatLng=null;
}
}
最后我们在"确定"按钮的点击事件上调用MarkerTranslateAnimator的startAnimation方法来开始动画
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.order_btn:
drivingRoutePresent.geocoder(fromET.getText().toString(), DrivingRoutePresent.FROM_TYPE);
drivingRoutePresent.geocoder(toET.getText().toString(), DrivingRoutePresent.TO_TYPE);
confirmBtn.setVisibility(View.VISIBLE);
orderBtn.setVisibility(View.GONE);
break;
case R.id.confirm_btn:
//开启动画移动
mAnimator.startAnimation();
orderBtn.setVisibility(View.VISIBLE);
confirmBtn.setVisibility(View.GONE);
break;
case R.id.location_ib:
//定位当前位置
break;
}
}
6.引入腾讯Android定位SDK
基本效果已经完成了,现在还差最后一个定位功能,要实现定位功能需要引入另一个SDK(Android定位SDK)
我们打开Android定位SDK开发文档,下载最新的SDK
将压缩包内的jar包放入app的libs包下,并添加为依赖
打开AndroidManifest.xml文件,加入下面权限配置
7.显示当前定位
配置完成后,我们在DrivingRouteContract.Presenter接口中加入一个开始定位的startLocation和一个结束定位的stopLocation方法
void startLocation(boolean single);
void stopLocation();
再实现一下DrivingRoutePresent的方法
private boolean IS_SINGLE_LOCATION_MODE = false;//是否连续定位
private TencentLocationManager mLocationManager = TencentLocationManager.getInstance(GlobalApplication.getContext());
private TencentLocationRequest locationRequest;
@Override
public void startLocation(boolean single) {
IS_SINGLE_LOCATION_MODE = single;//因为这里只需要定位一次,所以加了个参数
locationRequest = TencentLocationRequest.create();
locationRequest.setInterval(5000);//定位间隔
//根据用户获取的位置信息的详细程度,REQUEST_LEVEL_ADMIN_AREA:包含经纬度,位置所处的中国大陆行政区划
locationRequest.setRequestLevel(TencentLocationRequest.REQUEST_LEVEL_ADMIN_AREA);
locationRequest.setAllowGPS(true);//是否允许使用GPS定位
mLocationManager.requestLocationUpdates(locationRequest, this);//连续定位
}
@Override
public void stopLocation() {
mLocationManager.removeUpdates(this);
}
除此之外,为了获得定位的位置信息,我们还需要让DrivingRoutePresent额外实现TencentLocationListener接口,实现onLocationChanged(用于接收定位结果)和onStatusUpdate(用于接收GPS,WiFi,Cell的状态码)方法。
@Override
public void onLocationChanged(TencentLocation tencentLocation, int i, String s) {
if (IS_SINGLE_LOCATION_MODE)
stopLocation();
switch (i){
case TencentLocation.ERROR_OK:
//定位成功
drinvingRouteView.setLocation(tencentLocation);
//渲染定位信息
if (drinvingRouteView.getFromET()!=null&&drinvingRouteView.getFromET().getText().toString().trim().equals(""))
drinvingRouteView.getFromET().setText(tencentLocation.getAddress());
// Toast.makeText(GlobalApplication.getContext(), "定位成功", Toast.LENGTH_SHORT).show();
break;
case TencentLocation.ERROR_NETWORK:
Toast.makeText(GlobalApplication.getContext(), "网络问题引起的定位失败", Toast.LENGTH_SHORT).show();
break;
case TencentLocation.ERROR_BAD_JSON:
Toast.makeText(GlobalApplication.getContext(), "GPS, Wi-Fi 或基站错误引起的定位失败", Toast.LENGTH_SHORT).show();
break;
case TencentLocation.ERROR_WGS84:
Toast.makeText(GlobalApplication.getContext(), "无法将WGS84坐标转换成GCJ-02坐标时的定位失败", Toast.LENGTH_SHORT).show();
break;
case TencentLocation.ERROR_UNKNOWN:
Toast.makeText(GlobalApplication.getContext(), "未知原因引起的定位失败", Toast.LENGTH_SHORT).show();
break;
}
}
@Override
public void onStatusUpdate(String s, int i, String s1) {
//TencentLocationListener回调此方法传入的GPS,WiFi,Cell状态码,具体状态码查看Android定位SDK开发文档
}
最后,我们再把给定位的小按钮绑定的点击事件加上实现,在onResume和onPause方法调用一下startLocation和stopLocation方法让app在开启或切换回当前Activity时自动定位
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.order_btn:
drivingRoutePresent.geocoder(fromET.getText().toString(), DrivingRoutePresent.FROM_TYPE);
drivingRoutePresent.geocoder(toET.getText().toString(), DrivingRoutePresent.TO_TYPE);
confirmBtn.setVisibility(View.VISIBLE);
orderBtn.setVisibility(View.GONE);
break;
case R.id.confirm_btn:
//开启动画移动
mAnimator.startAnimation();
orderBtn.setVisibility(View.VISIBLE);
confirmBtn.setVisibility(View.GONE);
break;
case R.id.location_ib:
//定位一次
drivingRoutePresent.startLocation(true);
break;
}
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
drivingRoutePresent.startLocation(true);
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
drivingRoutePresent.stopLocation();
}
结尾
写到这里,效果图上所有的功能就基本完成了,总的来说,功能还是十分强大的,对于有相关需求的企业来说开发起来非常省时省力。另外开发文档和接口文档也比较详细。由于时间有限,暂时只体验了其中的几个服务,有更多需求的同学可以自行到官网探索。