Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一个简单的基于Leaflet样式的多图层安卓地图应用 #3

Open
KaidiGuo opened this issue Jan 31, 2017 · 0 comments
Open

Comments

@KaidiGuo
Copy link
Owner

BULABULA

这是 Mobile Cartography 课程的 project,看DDL还有四天就不慌不慢的花了三个半天写了个差不多,但是因为心态爆的太早了导致获取用户地理位置的功能一直出现fatal error虽然后来发现问题很明显却还是花了整整一天半才改掉。就很气。就决定来写这篇文章降降燥。
Map in Colour 的设计初衷是希望能给那些喜欢自由的探索一个城市的少年们一个较为直观的,城市内功能性POI分布的类别地图。
无需太多复杂的功能,我就是想展示,那条大街上密布餐馆,而那条大街上全是博物馆etc.....
Git地址.

Map in Colour

主界面UI部分结构介绍

用DrawerLayout+Toolbar实现。
DrawerLayout 的第一部分子布局既是主视图,第二部分android.support.design.widget.NavigationView则是侧边栏。可参考简书这篇文章,Android DrawerLayout.
主布局文件activity_maps.xml结构如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout>
      <LinearLayout>
       |    <android.support.v7.widget.Toolbar>
       |    <RelativeLayout>
       |     |     <Button>
       |     |     <WebView>
       |     </RelativeLayout>
     </LinearLayout>
     <android.support.design.widget*NavigationView... >
</android.support.v4.widget.DrawerLayout>

在LinerLayout主视图内,布局从上到下为一个Toolbar,一个RelativeLayout /(Button+WebView).
这个Button是因为leaflet地图没有提供好用的用户位置location API,必须要自己写一个按钮来实现Location+SetMapView的功能. WebView 用来显示HTML文件.

A WebView is a View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.
Note that, in order for your Activity to access the Internet and load web pages in a WebView, you must add the INTERNET permission to your Android Manifest file as a child of the element:
<uses-permission android:name="android.permission.INTERNET" />
By default, a WebView provides no browser-like widgets, does not enable JavaScript and web page errors are ignored. If your goal is only to display some HTML as a part of your UI, this is probably fine; the user won't need to interact with the web page beyond reading it, and the web page won't need to interact with the user. If you actually want a full-blown web browser, then you probably want to invoke the Browser application with a URL Intent rather than show it with a WebView.

所以,也就是说,用户是和这个MapView没有任何互动的. 但是当然这个工程也没有考虑这个需求 XD

主界面里Toolbar, DrawerLayout, NavigationView,WebView的初始化

Toolbar

首先,在主Activity,MapsActivity.java里初始化Toolbar. 最后一行 myToolbar.inflateMenu 用来关联一个menu文件夹下的menu布局文件,实现 Toolbar 右侧的下拉菜单.

Toolbar myToolbar = (Toolbar) findViewById(my_toolbar);
myToolbar.setTitle("Map in colour");
myToolbar.setTitleTextColor(Color.WHITE); 
myToolbar.inflateMenu(R.menu.button_clear);

Toolbar右侧下拉菜单布局是放在res/menu文件夹下的button_clear.xml文件.
内部有两个item,help和info.
点击Help后启动第二个Activity来展示图例说明.
点击info后在主界面上显示一段文字(开发者信息).

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_item"
        android:title="@string/menu_item_01"
        app:showAsAction="never" />
    <item
        android:id="@+id/action_item0"
        android:title="@string/menu_item_02"
        app:showAsAction="never" />
</menu>

设定点击menu中item的响应:

myToolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                int menuItemId = item.getItemId();
                if (menuItemId == R.id.action_item) {
                    onClickStartNewActivity();
                } else if (menuItemId == R.id.action_item0) {
                    Toast.makeText(MapsActivity.this , "..........", Toast.LENGTH_LONG).show();
                }
                return true;
            }
        });

DrawerLayout

然后是继续在Activity 初始化DrawerView以实现Toolbar左侧的坍缩按钮和侧边栏的关联.
参考Android开发指南 ActionBarDrawerToggle
参数如下:
ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, int drawerImageRes, int openDrawerContentDescRes, int closeDrawerContentDescRes)

初始化如下:

final DrawerLayout mDrawerlayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this,
                mDrawerlayout,
                myToolbar,
                R.string.app_name,
                R.string.app_name
        );
        mDrawerlayout.addDrawerListener(toggle);
        toggle.syncState();

NavigationView

然后是继续初始化侧边栏布局NavigationView.

        final NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
        navigationView.setItemIconTintList(null);

侧边栏NavigationView由两部分组成.
在主布局文件activity_maps.xml中可以看到:

<android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/activity_main_drawer"
        app:itemTextColor="@color/drawer_item"
        app:itemIconTint="@color/drawer_item_tint"
        />

这个 app:headerLayoutapp:menu 分别指定了侧边栏的头部分布局,和头部分下面的菜单布局。
既,上部图片区,和下部列表区。
头部nav_header.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="192dp"
    android:theme="@style/ThemeOverlay.AppCompat.Dark">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorAccent"
        android:src="@drawable/logopic"
        android:scaleType="centerCrop"/>
</FrameLayout>

菜单部:

<?xml version="1.0" encoding="utf-8"?>
<menu
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:title="@string/selectLayer">
        <menu>
            <item
                android:id="@+id/nav_eat"
                android:icon="@drawable/ic_1"
                android:title="@string/eat"/>
             ......         
            <item
                android:id="@+id/nav_wc"
                android:icon="@drawable/ic_8"
                android:title="@string/wc"/>
        </menu>
    </item>
</menu>

为什么结构是item内部menu内部item呢?
因为我希望这个菜单可以多个item选中,而不是点选一个后之前check掉的那个又被清除了.
而:

Checkable items appear only in submenus or context menus.

然后是设定点击menu里面item的响应:

navigationView.setNavigationItemSelectedListener(newNavigationView.OnNavigationItemSelectedListener() {
       @Override
        public boolean onNavigationItemSelected(MenuItem item) {......}
}

WebView

初始化WebView用来显示地图.

 final WebView webView = (WebView) findViewById(webview);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.addJavascriptInterface(this, "javatojs");
        webView.loadUrl("file:///android_asset/map.html");

这里需要在java文件夹下新建一个asset文件夹用来存放HTML文件.

<!DOCTYPE html>
<html>
<head>
    <title>Custom Map</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"/>
    <script src="https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"></script>
    <style>......</style>
</head>
<body>
<div id='map'></div>
<script>......</script>
</body>
</html>

script里面为关键内容.
先设定地图的初始显示中心和缩放等级:
var map = L.map('map', {center: [51.051877, 13.741517],zoom: 15});

再加入你先设计好样式的底图图层链接:

var basemap = L.tileLayer('HERE YOUR BASE MAP URL',{
           id: 'mapbox.streets'
       });
    basemap.addTo(map);

接下来是一个用来传入获取到的用户坐标重设map中心点的功能:

 function relocation(i,j){
     var london = new L.LatLng(i,j);
     map.setView(london, 17);
    }

在这里,我们需要Mapbox来发布自定义地图样式.

For customizing the style of our Mapbox map, please login in to Mapbox Studio or create an account if you don’t have one yet. After logging in, select the option New style. You can give your style a name and choose if you want to modify an existing Mapbox style or create a completely new one whereas the last option is less comfortable since the new style needs to be build up from the bottom. After clicking Create you can modify the look of all objects in the map up to your needs. Therefore select an object for opening an additional window. The tab Style contains control elements for editing the appearance of the respective object, the tab Select data offers further options, for instance defining a scale range in which the selected object is visible in the map. Furthermore more you can add, hide, duplicate and delete layers.
After creating your own map style, click on the Publish-button in the upper left corner. A pop up window will inform you whether the publishing process was successful. If so, click Preview, develop & use. On the new page scroll down to the section Develop with this style and switch from the Mapbox to the Leaflet option. A Leaflet URL is displayed to you which needs to be copied and used for replacing the URL in the tileLayer-definition in map.html. Now a map with the style you have just created will be displayed in the Android app after re-running it.

Leaflet URL

在这个工程里,我们一共发布了九个图层,分别是一个黑底白色label的底图,和剩下八个单独色点背景透明的,选择好了POI类别的图层.
在script里面接着声明他们,并写8个添加他们的function,和8个删除他们的function :
(好吧小白表示这里代码冗余也不晓得怎么管了,以后再学习处理吧.....)

var layer1 = L.tileLayer('https://api.mapbox.com......',
        {
            id: 'mapbox.streets'
        });
function addlayer1(){
        layer1.addTo(map);
    }
function removelayer1(){
         map.removeLayer(layer1);
    }

到这里,HTML就全部写完了,这八个add layer的方法和上面提到的侧边栏NavigationView里面menu的每个item相对应,当public boolean onNavigationItemSelected(MenuItem item) {......}触发,在此方法内部实现分别调用JS里面的function来把图层添加或移除.

获得用户地理位置的方法

首先在Mainifest里添加许可:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

初始化LocationListener,用于捕获用户地理位置变更和相应的响应事件 .

locationListener = new LocationListener() {
            public void onLocationChanged(Location location) {
            }
            public void onProviderDisabled(String provider) {
            }
            public void onProviderEnabled(String provider) {
            }
            public void onStatusChanged(String provider, int status, Bundle extras) {
            }
};

LocationManager获取系统(定位LOCATION_SERVICE)服务 .
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

然而,在调用任何LocationManager方法之前,我们被要求必须先检查用户许可.
这里的if就是,如果检测到我们还没有问用户申请过许可,就必须在这里先申请.

 if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            ActivityCompat.requestPermissions(this, LOCATION_PERMS , 1340);
            return;
        }

一旦当我们获得过了用户许可,检查许可就可以通过,则跳过if语句,调用LocationManager方法被允许.

location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
final double latitude = location.getLatitude();
final double longitude = location.getLongitude();

上面提到,如果许可检查失败,在if里面我们需要向用户提出许可申请.
这里要使用 ActivityCompat.requestPermissions(this, LOCATION_PERMS , 1340) 方法.
根据安卓开放说明,三个变量如下:

requestPermissions(Activity activity, String[] permissions, int requestCode)

第二个变量 String[] permissions 在公共类开头以被声明.
final String[] LOCATION_PERMS = {android.Manifest.permission.ACCESS_FINE_LOCATION};
第三个变量也一样:
final int LOCATION_REQUEST = 1340;
根据 Android help

一旦 requestPermissions 被调用,会有一个pop up窗口弹出问用户申请许可.

If your app does not have the requested permissions the user will be presented with UI for accepting them. After the user has accepted or rejected the requested permissions you will receive a callback reporting whether the permissions were granted or not. Your activity has to implement ActivityCompat.OnRequestPermissionsResultCallback and the results of permission requests will be delivered to its onRequestPermissionsResult(int, String[], int[]) method.

当我们调用 requestPermissions 之后, 我们需要重写 onRequestPermissionsResult 方法,在
OnCreate() 外面. 如果用户返回结果是 "accept",接着即可调用 LocationManager的 Location updates方法了.

@Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode){
            case LOCATION_REQUEST:
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                    locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,1000, 0, locationListener);
                    onClickStartNewActivity();//First time start : show second view
                } else {
                    Toast.makeText(this, "Location cannot be obtained due to " + "missing permission.", Toast.LENGTH_LONG).show();
                }
                break;
        }
    }

我在这里多写了一句调用onClickStartNewActivity()方法来打开第二个Activity用以刷新主Activity (用户许可会被系统记住,再打开主Activity时 if 检查就会通过,if后面的代码就可以生效了 )

       final String position= "javascript:relocation("+latitude+","+longitude+")";
       final Button button = (Button) findViewById(R.id.locationbutton);
       button.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                webView.loadUrl(position);
                //Toast.makeText(MapsActivity.this , position, Toast.LENGTH_LONG).show();
                // Perform action on click
            }

onClickStartNewActivity()方法如下:

public void onClickStartNewActivity() {
        Intent intent = new Intent(this, LegendActivity.class);
        startActivity(intent);
 }

第二个Activity就是说明界面. 较为简单在这里就不细说了.

应用语言自适应

所有的系统String都放入资源文件夹内的strings.xml内.

<resources>
    <string name="app_name">Map in colour</string>
    <string name="title_activity_maps">Map in colour</string>
    <string name="title_help">How does it work</string>
    <string name="menu_item_01">Help</string>
    <string name="menu_item_02">Info</string>
    <string name="selectLayer">Select layers</string>
    <string name="eat">Eat&Drink</string>
    <string name="buy">Buy&Buy</string>
    <string name="culture">Art&Culture</string>
    <string name="club">Entertain&Nightlife</string>
    <string name="transport">Come&Go</string>
    <string name="hospital">Hospital&Pharmacy</string>
    <string name="bank">Bank&Exchange</string>
    <string name="wc">Pee&Poo</string>
</resources>

再在res文件夹内新建一个名为values-zh的文件夹. 也放入一个strings.xml.
但这个文件设定为中文.

<resources>
    <string name="app_name">彩色地图</string>
    <string name="title_activity_maps">彩色地图</string>
    <string name="title_help">图层说明</string>
    <string name="menu_item_01">帮助</string>
    <string name="menu_item_02">信息</string>
    <string name="selectLayer">选择图层</string>
   <string name="eat">吃吃喝喝</string>
    <string name="buy">买买买</string>
    <string name="culture">艺术与文化</string>
    <string name="club">酒吧与夜生活</string>
    <string name="transport">交通枢纽</string>
    <string name="hospital">医院</string>
    <string name="bank">银行与货币兑换</string>
    <string name="wc">厕所</string>
</resources>

这样,应用就会根据系统设定的语言来自主选择从哪一个资源文件内选择string了.

结尾处的BULABULA

现在的问题是,POI在小比例尺的视图中都会消失不见,这个scale level能否改变一下,使得在小比例尺视图中可以看到全城的概览就是下一个需要考虑的问题啦~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant