开箱即用 Google地图实现钉钉打卡功能

1,417 阅读6分钟

Google地图设置Geofence并监听位置的进入

业务场景

类似钉钉的设置公司位置,并设置一个打卡范围,当员工进入范围之后自动上班打卡。

实现流程

  1. 导入谷歌定位的Api
  2. 导入谷歌地图的依赖
  3. 开启持续定位并在地图上展示Marker
  4. 谷歌地图展示之后设置目标点 并设置Geofence栏栅画出范围圈圈
  5. 监听onLocationchanged 每次回调判断当前定位的位置是否在栏栅内。

代码如下:

根项目下的build.gradle

谷歌服务插件引入

    dependencies {
        classpath 'com.android.tools.build:gradle:7.0.3'

        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        classpath 'com.google.gms:google-services:4.3.8'
    }

app模块下的build.gradle最底部 应用谷歌服务插件

apply plugin: 'com.google.gms.google-services'

然后去谷歌后台申请App-Key相关的项目,需要填写包名和一些签名文件的SHA1值,注意这里relese包的SHA1值,和谷歌市场上架的SHA1值是不同的,上架之后需要去谷歌市场把谷歌市场上的SHA1值设置进去,因为谷歌市场上架之后会把你的签名文件包装一次,完全不同的签名文件了。

完成此步之后 下载对应App的 google-services.json 文件。

此步骤完成,我们再远程依赖谷歌定位的库。location

   //定位功能
    api 'com.google.android.gms:play-services-location:16.0.0'
    //Google地图
    api 'com.google.android.gms:play-services-maps:16.0.0'

定位Api使用流程:

权限定义

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

清单文件的配置

        <!--   Google-Map  配置文件 ↓ -->
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <!--指定GoogleMapAppKey 动态的赋值,在config.gradle中配置-->
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${google_cloud_api_key}" />

使用之前要动态申请权限,这里不做演示。

       <!--谷歌地图-->
         <fragment
            android:id="@+id/check_in_out_map"
            android:name="com.google.android.gms.maps.SupportMapFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@+id/ll_bottom_group"
         />

初始化Map与Google定位

        //Map的是否准备完成回调
        mMapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.check_in_out_map);
        mMapFragment.onCreate(savedInstanceState);
        mMapFragment.getMapAsync(this);

        private void createGoogleApiClient() {
        if (googleApiClient == null) {
            googleApiClient = new GoogleApiClient.Builder(mContext)
                    .addConnectionCallbacks(this)
                    .addOnConnectionFailedListener(this)
                    .addApi(LocationServices.API)
                    .build();
        }
        //默认开启
        googleApiClient.connect();
    } 

Map与定位的回调

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;
        LogUtil.e("onMapReady:地图以及准备好了");
        mMap.getUiSettings().setZoomControlsEnabled(false);
        mMap.getUiSettings().setCompassEnabled(true);
        mMap.getUiSettings().setMyLocationButtonEnabled(true);
        mMap.getUiSettings().setAllGesturesEnabled(true);
    }


    /**
     * Google地图回调,连接成功
     */
    @Override
    public void onConnected(@Nullable Bundle bundle) {
        LogUtil.e("Google定位服务连接成功了");
        getLastLocation();
    }

    @Override
    public void onConnectionSuspended(int i) {
        LogUtil.e("Google定位服务被暂停了");
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        LogUtil.e("Google定位服务连接错误");
    }


    private void getLastLocation() {
        if (checkPermission()) {
            mMap.setMyLocationEnabled(true);
            if (mCurrentLocation != null) {
                mCurrentLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
                LogUtil.e("getLastLocation成功获取到最后的定位纬度:" + mCurrentLocation.getLatitude());
                LogUtil.e("getLastLocation成功获取到最后的定位经度:" + mCurrentLocation.getLongitude());

                //每次回调都算一下距离目标点有多少米
                getRestDistance(mCurrentLocation);

                startLocationUpdates();
            } else {
                startLocationUpdates();
            }
        } else {
            askPermission();
        }
    }

开启持续定位的逻辑

    /**
     * 开始监听地址位置的变化监听
     */
    private void startLocationUpdates() {
        createLocationRequest();
        if (checkPermission()) {
            LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, mLocationRequest, this);
        }
    }

    /**
     * 创建位置变化监听服务,10秒一次循环监听
     */
    protected void createLocationRequest() {
        mLocationRequest = LocationRequest.create()
                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                //设置循环的时间
                .setInterval(8000)
                //设置最小的移动范围
                .setSmallestDisplacement(10)
                .setFastestInterval(8000);
        if (checkPermission()) {
            LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, mLocationRequest, this);
        }
    }

    //持续定位的回调
    @Override
    public void onLocationChanged(Location location) {
        mCurrentLocation = location;
        LogUtil.e("onLocationChanged成功获取到定位纬度:" + location.getLatitude());
        LogUtil.e("onLocationChanged成功获取到定位经度:" + location.getLongitude());
        //每次回调都算一下距离目标点有多少米
        getRestDistance(mCurrentLocation);

        setCurrentDate();

    }


    /**
     * 获取目标距离目的地还有多少距离
     */
    private void getRestDistance(Location location) {

        if (TextUtils.isEmpty(mJobLatitude) || TextUtils.isEmpty(mJobLongitude)) {
            return;
        }

        String shortDistance = GoogleMapUtils.getInstance().getShortDistance(location.getLongitude(), location.getLatitude()
                , Double.parseDouble(mJobLongitude), Double.parseDouble(mJobLatitude));

        if (Double.parseDouble(shortDistance) <= Constants.GEOFENCE_RADIUS) {
            LogUtil.e("shortDistance == > 距离:" + shortDistance + "米" + "可以签到!");
            //在范围内部了,可以签到了
            isInsideGeofence = true;
        } else {
            LogUtil.e("shortDistance == > 距离:" + shortDistance + "米" + "不可以签到!");
            isInsideGeofence = false;
        }

    }

测距的工具类:

public class GoogleMapUtils {

    private double DEF_PI = 3.14159265359; // PI
    private double DEF_2PI = 6.28318530712; // 2*PI
    private double DEF_PI180 = 0.01745329252; // PI/180.0
    private double DEF_R = 6370693.5; // radius of earth

    private GoogleMapUtils(){}
    private static GoogleMapUtils instance;

    public static synchronized GoogleMapUtils getInstance(){
        if(instance == null){
            instance = new GoogleMapUtils();
        }
        return instance;
    }


    /**
     * 返回为m,适合短距离测量
     */
    public String getShortDistance(double lon1, double lat1, double lon2, double lat2) {
        double ew1, ns1, ew2, ns2;
        double dx, dy, dew;
        double distance;
        // 角度转换为弧度
        ew1 = lon1 * DEF_PI180;
        ns1 = lat1 * DEF_PI180;
        ew2 = lon2 * DEF_PI180;
        ns2 = lat2 * DEF_PI180;
        // 经度差
        dew = ew1 - ew2;
        // 若跨东经和西经180 度,进行调整
        if (dew > DEF_PI)
            dew = DEF_2PI - dew;
        else if (dew < -DEF_PI)
            dew = DEF_2PI + dew;
        dx = DEF_R * Math.cos(ns1) * dew; // 东西方向长度(在纬度圈上的投影长度)
        dy = DEF_R * (ns1 - ns2); // 南北方向长度(在经度圈上的投影长度)
        // 勾股定理求斜边长
        distance = Math.sqrt(dx * dx + dy * dy);
        return trans(distance);
    }



    /**
     * 返回为m,适合长距离测量
     */
    public String getLongDistance(double lon1, double lat1, double lon2, double lat2) {
        double ew1, ns1, ew2, ns2;
        double distance;
        // 角度转换为弧度
        ew1 = lon1 * DEF_PI180;
        ns1 = lat1 * DEF_PI180;
        ew2 = lon2 * DEF_PI180;
        ns2 = lat2 * DEF_PI180;
        // 求大圆劣弧与球心所夹的角(弧度)
        distance = Math.sin(ns1) * Math.sin(ns2) + Math.cos(ns1) * Math.cos(ns2)
                * Math.cos(ew1 - ew2);
        // 调整到[-1..1]范围内,避免溢出
        if (distance > 1.0)
            distance = 1.0;
        else if (distance < -1.0)
            distance = -1.0;
        // 求大圆劣弧长度
        distance = DEF_R * Math.acos(distance);
        return trans(distance);
    }


    private String trans(double distance) {

        return (new DecimalFormat(".00").format(distance));
    }

覆盖物点击Adapter由于是内部的项目UI,每个项目都不同,大家可以自己实现,和ListView的Adapter类似:

public class CheckinMapInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {

    private final View contentsView;
    private CheckInDetail.DataBean jobSchedule;

    public CheckinMapInfoWindowAdapter(Context context, CheckInDetail.DataBean jobSchedule) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        contentsView = inflater.inflate(R.layout.layout_map_info_window, null, false);
        this.jobSchedule = jobSchedule;
    }

    @Override
    public View getInfoWindow(Marker marker) {
        return null;
    }

    @Override
    public View getInfoContents(Marker marker) {

        TextView employerTV = contentsView.findViewById(R.id.text_view_job_employer);
        TextView jobTimeTV = contentsView.findViewById(R.id.text_view_time);
        TextView jobLocationTV = contentsView.findViewById(R.id.text_view_job_location);
        TextView jobRateTV = contentsView.findViewById(R.id.text_view_job_rate);
        TextView jobRoleTV = contentsView.findViewById(R.id.text_view_job_role);

        employerTV.setText(jobSchedule.getJob_employer_company_name());

        String startTime = DateAndTimeUtil.stampToDate3(jobSchedule.getJob_start_date() + "");
        String endTime = DateAndTimeUtil.stampToDate3(jobSchedule.getJob_end_date() + "");
        String scheduleStr = startTime + " - " + endTime;
        jobTimeTV.setText(scheduleStr);

        jobLocationTV.setText(jobSchedule.getJob_address());

        String rateStr = "Up to $" + String.valueOf((int) jobSchedule.getJob_hour_rate()) + "/Hr";
        jobRateTV.setText(rateStr);

        jobRoleTV.setText(jobSchedule.getJob_title());

        return contentsView;
    }
}

定位与测算的逻辑完成了,地图上也能展示我的位置了。那我们开始绘制目标点与范围相关的Map绘制。

我们需要请求接口获取到工作点的相关信息,然后再Map上绘制 代码如下:

 /**
     * 展示下面的详情信息
     *
     * @param jobSchedule
     */
    private void setScheduleDetails(CheckInDetail.DataBean jobSchedule) {

        if (jobSchedule != null) {
            //Job的工作点
            final LatLng jobLatLng = new LatLng(Double.parseDouble(jobSchedule.getJob_latitude()),
                    Double.parseDouble(jobSchedule.getJob_longitude()));

            jobMarker = mMap.addMarker(new MarkerOptions().position(jobLatLng)
                    .icon(BitmapDescriptorFactory.fromResource(R.drawable.address_red)));
            int zoom = (int) 15f;
            //地图移动到工作点的位置
            mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(jobLatLng.latitude +
                    (double) 90 / Math.pow(2, zoom), jobLatLng.longitude), zoom));
            //设置Mark覆盖物的点击事件与弹窗选择,如果没这样的需要可以不要
            //由于每个项目的弹窗UI不一致,这里大家自己实现即可
            if (mMap != null) {
                mMap.setInfoWindowAdapter(new CheckinMapInfoWindowAdapter(mActivity, jobSchedule));
                jobMarker.showInfoWindow();
            }
            mMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
                @Override
                public boolean onMarkerClick(Marker marker) {
                    int zoom = (int) 15f;
                    mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new
                            LatLng(jobLatLng.latitude + (double) 90 / Math.pow(2, zoom),
                            jobLatLng.longitude), zoom));

                    jobMarker.showInfoWindow();

                    return true;
                }
            });

            isInsideGeofence = false;
            //添加栏栅范围效果
            startGeofence();
        } else {
            showHideEmptyState(View.VISIBLE, getString(R.string.empty_state_check_in));
            checkInRefreshLayout.setEnabled(true);
        }
    }

添加栏栅范围效果,我们设置的是300米范围

    private void startGeofence() {
        LogUtil.e("开始在谷歌地图上面画栏栅");
        //1.先创建栏栅
        Geofence geofence = createGeofence(jobMarker.getPosition(), Constants.GEOFENCE_RADIUS);
        //2.在创建栏栅的请求
        GeofencingRequest geofencingRequest = createGeofenceRequest(geofence);
        //3.添加栏栅到地图上
        addGeofence(geofencingRequest);
    }

    /**
     * 监听如果进入或者出去栏栅那么会调用pendingintent里面的方法
     * pendingintent进入了一个intentService,发送notify
     */
    private void addGeofence(GeofencingRequest geofencingRequest) {
        if (checkPermission()) {
            if (googleApiClient != null && googleApiClient.isConnected()) {
                LocationServices.GeofencingApi.addGeofences(googleApiClient,
                        geofencingRequest, createGeofencingPendingIntent()).setResultCallback(this);
            }
        }
    }

    private PendingIntent createGeofencingPendingIntent() {
        if (mGeofencingPendingIntent != null) {
            return mGeofencingPendingIntent;
        }

        Intent intent = new Intent(mContext, GeofenceTransitionsIntentService.class);
        mGeofencingPendingIntent = PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        return mGeofencingPendingIntent;
    }

    private GeofencingRequest createGeofenceRequest(Geofence geofence) {
        return new GeofencingRequest.Builder()
                .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
                .addGeofence(geofence)
                .build();
    }

    private Geofence createGeofence(LatLng latLng, float radius) {
        return new Geofence.Builder()
                //设置栏栅的ID
                .setRequestId(GEOFENCE_REQ_ID)
                //设置圆形经纬度起点和半径(米)
                .setCircularRegion(latLng.latitude, latLng.longitude, radius)
                //设置存在的时间,这里是永远存在
                .setExpirationDuration(Geofence.NEVER_EXPIRE)
                //设置监听的方式:进来或者出去
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)
                .build();
    }

    /**
     * 回调成功才开始画栏栅
     */
    @Override
    public void onResult(@NonNull Status status) {
       if (status.isSuccess()) {
           drawGeofence();
       }
    }

    //画出范围圆形
    private void drawGeofence() {
        LogUtil.e("回调成功,开始画圆形范围");
        if (geofenceLimits != null) {
            geofenceLimits.remove();
        }

        CircleOptions circleOptions = new CircleOptions()
                //中心点位marker的位置
                .center(jobMarker.getPosition())
                //空心边框颜色
                .strokeColor(Color.argb(180, 200, 5, 0))
                //边框宽度
                .strokeWidth(ScreenUtils.dpToPx(mContext, 0.9f))
                //实心填充颜色
                .fillColor(Color.argb(25, 200, 5, 0))
                //圆形半径,这里是300
                .radius(300);
        //在地图上添加圆形范围
        geofenceLimits = mMap.addCircle(circleOptions);
    }

看上面的代码我们知道是否进入了范围是传入了一个IntentService。

IntentService内部会判断是否进入了范围,是否需要发送事件等,代码如下

public class GeofenceTransitionsIntentService extends IntentService {

    private static final String TAG = GeofenceTransitionsIntentService.class.getSimpleName();

    public GeofenceTransitionsIntentService() {
        super(TAG);
    }

    public GeofenceTransitionsIntentService(String name) {
        super(name);
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);

        // Handling errors
        if ( geofencingEvent.hasError() ) {
            String errorMsg = getErrorString(geofencingEvent.getErrorCode() );
            return;
        }

        int geofenceTransition = geofencingEvent.getGeofenceTransition();
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
            List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();

            String geofenceTransitionDetails = getGeofenceTransitionDetails(geofenceTransition, triggeringGeofences);
            sendNotification(geofenceTransitionDetails, geofenceTransition);
        }
    }

    //主要判断逻辑在此
    private String getGeofenceTransitionDetails(int geofenceTransition, List<Geofence> triggeringGeofences) {
        ArrayList<String> triggeringGeofencesList = new ArrayList<>();
        for ( Geofence geofence : triggeringGeofences ) {
            triggeringGeofencesList.add( geofence.getRequestId() );
        }

        String status = null;
        if ( geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ) {
            status = "Entering job location";
            LogUtil.e("GeofenceTransitionsIntentService:监听到了进来了,然后设置为在范围内");
            CheckInFragment.isInsideGeofence = true;
        } else if ( geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT ) {
            status = "Exiting job location";
            LogUtil.e("GeofenceTransitionsIntentService:监听到了进来了,然后设置不在范围内");
            CheckInFragment.isInsideGeofence = false;
        }
        return status;
    }

    /**
     * 发送通知的逻辑,我们可以自定义实现,可以在通知栏发送通知,或者EventBus/广播 通知上级页面处理签到逻辑
     */
    private void sendNotification(String msg, int geofenceTransition) {

        Intent notificationIntent = MainActivity.makeNotificationIntent(getApplicationContext(), msg, geofenceTransition);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
        stackBuilder.addParentStack(MainActivity.class);
        stackBuilder.addNextIntent(notificationIntent);
        PendingIntent notificationPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

        // Creating and sending Notification
        //自己实现
    }

    // Handle errors
    private static String getErrorString(int errorCode) {
        switch (errorCode) {
            case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE:
                return "GeoFence not available";
            case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES:
                return "Too many GeoFences";
            case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS:
                return "Too many pending intents";
            default:
                return "Unknown error.";
        }
    }
}

谷歌定位与谷歌地图监听位置进入的逻辑到此结束。

其实我们可以看到,他们是一体的,Geofence 设置的位置对象是 googleApiClient 定位对象。

如果换想用百度定位实现谷歌地图的签到逻辑是行不通了。我们只能在百度的持续定位中每次计算距离目的地还有多少米,这样可能效果不是那么的好,因为谷歌地图上的当前位置点是谷歌定位提供的,移动起来比较平滑,而使用百度定位进行mark显示,只能每10秒持续定位成功之后换一次mark显示,效果比较割裂。

推荐大家涉及到地图移动这一块还是使用谷歌地图结合谷歌定位实现。