BULABULA
這是 Mobile Cartography 課程的 project,看DDL還有四天就不慌不慢的花了三個(gè)半天寫了個(gè)差不多屿储,但是因?yàn)樾膽B(tài)爆的太早了導(dǎo)致獲取用戶地理位置的功能一直出現(xiàn)fatal error雖然后來發(fā)現(xiàn)問題很明顯卻還是花了整整一天半才改掉昭灵。就很氣吠裆。就決定來寫這篇文章降降燥。
Map in Colour 的設(shè)計(jì)初衷是希望能給那些喜歡自由的探索一個(gè)城市的少年們一個(gè)較為直觀的烂完,城市內(nèi)功能性POI分布的類別地圖试疙。
無需太多復(fù)雜的功能,我就是想展示抠蚣,那條大街上密布餐館祝旷,而那條大街上全是博物館etc.....
Git地址.
![Map in Colour](https://github.com/Sublunarwind/Map-in-Colour/raw/master/demo.gif)
主界面UI部分結(jié)構(gòu)介紹
用DrawerLayout+Toolbar實(shí)現(xiàn)。
DrawerLayout 的第一部分子布局既是主視圖嘶窄,第二部分android.support.design.widget.NavigationView則是側(cè)邊欄怀跛。可參考簡書這篇文章柄冲,Android DrawerLayout.
主布局文件activity_maps.xml結(jié)構(gòu)如下:
<?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主視圖內(nèi)吻谋,布局從上到下為一個(gè)Toolbar,一個(gè)RelativeLayout /(Button+WebView).
這個(gè)Button是因?yàn)閘eaflet地圖沒有提供好用的用戶位置location API现横,必須要自己寫一個(gè)按鈕來實(shí)現(xiàn)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 <manifest> 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.
所以漓拾,也就是說,用戶是和這個(gè)MapView沒有任何互動(dòng)的. 但是當(dāng)然這個(gè)工程也沒有考慮這個(gè)需求 XD
主界面里Toolbar, DrawerLayout, NavigationView,WebView的初始化
Toolbar
首先戒祠,在主Activity晦攒,MapsActivity.java里初始化Toolbar. 最后一行 myToolbar.inflateMenu 用來關(guān)聯(lián)一個(gè)menu文件夾下的menu布局文件,實(shí)現(xiàn) Toolbar 右側(cè)的下拉菜單.
Toolbar myToolbar = (Toolbar) findViewById(my_toolbar);
myToolbar.setTitle("Map in colour");
myToolbar.setTitleTextColor(Color.WHITE);
myToolbar.inflateMenu(R.menu.button_clear);
Toolbar右側(cè)下拉菜單布局是放在res/menu文件夾下的button_clear.xml文件.
內(nèi)部有兩個(gè)item得哆,help和info.
點(diǎn)擊Help后啟動(dòng)第二個(gè)Activity來展示圖例說明.
點(diǎn)擊info后在主界面上顯示一段文字(開發(fā)者信息).
<?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>
設(shè)定點(diǎn)擊menu中item的響應(yīng):
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
然后是繼續(xù)在Activity 初始化DrawerView以實(shí)現(xiàn)Toolbar左側(cè)的坍縮按鈕和側(cè)邊欄的關(guān)聯(lián).
參考Android開發(fā)指南 ActionBarDrawerToggle
參數(shù)如下:
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
然后是繼續(xù)初始化側(cè)邊欄布局NavigationView.
final NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setItemIconTintList(null);
側(cè)邊欄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"
/>
這個(gè) app:headerLayout
和 app:menu
分別指定了側(cè)邊欄的頭部分布局脯颜,和頭部分下面的菜單布局。
既贩据,上部圖片區(qū)栋操,和下部列表區(qū)闸餐。
頭部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>
為什么結(jié)構(gòu)是item內(nèi)部menu內(nèi)部item呢?
因?yàn)槲蚁M@個(gè)菜單可以多個(gè)item選中矾芙,而不是點(diǎn)選一個(gè)后之前check掉的那個(gè)又被清除了.
而:
Checkable items appear only in submenus or context menus.
然后是設(shè)定點(diǎn)擊menu里面item的響應(yīng):
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文件夾下新建一個(gè)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" />
<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里面為關(guān)鍵內(nèi)容.
先設(shè)定地圖的初始顯示中心和縮放等級(jí):
var map = L.map('map', {center: [51.051877, 13.741517],zoom: 15});
再加入你先設(shè)計(jì)好樣式的底圖圖層鏈接:
var basemap = L.tileLayer('HERE YOUR BASE MAP URL',{
id: 'mapbox.streets'
});
basemap.addTo(map);
接下來是一個(gè)用來傳入獲取到的用戶坐標(biāo)重設(shè)map中心點(diǎn)的功能:
function relocation(i,j){
var london = new L.LatLng(i,j);
map.setView(london, 17);
}
在這里舍沙,我們需要Mapbox來發(fā)布自定義地圖樣式.
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](https://github.com/Sublunarwind/Map-in-Colour/raw/master/url1.png)
在這個(gè)工程里,我們一共發(fā)布了九個(gè)圖層剔宪,分別是一個(gè)黑底白色label的底圖拂铡,和剩下八個(gè)單獨(dú)色點(diǎn)背景透明的,選擇好了POI類別的圖層.
在script里面接著聲明他們葱绒,并寫8個(gè)添加他們的function感帅,和8個(gè)刪除他們的function :
(好吧小白表示這里代碼冗余也不曉得怎么管了,以后再學(xué)習(xí)處理吧.....)
var layer1 = L.tileLayer('https://api.mapbox.com......',
{
id: 'mapbox.streets'
});
function addlayer1(){
layer1.addTo(map);
}
function removelayer1(){
map.removeLayer(layer1);
}
到這里地淀,HTML就全部寫完了失球,這八個(gè)add layer的方法和上面提到的側(cè)邊欄NavigationView里面menu的每個(gè)item相對(duì)應(yīng),當(dāng)public boolean onNavigationItemSelected(MenuItem item) {......}觸發(fā)帮毁,在此方法內(nèi)部實(shí)現(xiàn)分別調(diào)用JS里面的function來把圖層添加或移除.
獲得用戶地理位置的方法
首先在Mainifest里添加許可:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
初始化LocationListener实苞,用于捕獲用戶地理位置變更和相應(yīng)的響應(yīng)事件 .
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獲取系統(tǒng)(定位LOCATION_SERVICE)服務(wù) .
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
然而,在調(diào)用任何LocationManager方法之前烈疚,我們被要求必須先檢查用戶許可.
這里的if就是黔牵,如果檢測(cè)到我們還沒有問用戶申請(qǐng)過許可,就必須在這里先申請(qǐng).
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;
}
一旦當(dāng)我們獲得過了用戶許可爷肝,檢查許可就可以通過猾浦,則跳過if語句,調(diào)用LocationManager方法被允許.
location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
final double latitude = location.getLatitude();
final double longitude = location.getLongitude();
上面提到阶剑,如果許可檢查失敗跃巡,在if里面我們需要向用戶提出許可申請(qǐng).
這里要使用 ActivityCompat.requestPermissions(this, LOCATION_PERMS , 1340) 方法.
根據(jù)安卓開放說明,三個(gè)變量如下:
requestPermissions(Activity activity, String[] permissions, int requestCode)
第二個(gè)變量 String[] permissions 在公共類開頭以被聲明.
final String[] LOCATION_PERMS = {android.Manifest.permission.ACCESS_FINE_LOCATION};
第三個(gè)變量也一樣:
final int LOCATION_REQUEST = 1340;
根據(jù) Android help
一旦 requestPermissions 被調(diào)用牧愁,會(huì)有一個(gè)pop up窗口彈出問用戶申請(qǐng)?jiān)S可.
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.
當(dāng)我們調(diào)用 requestPermissions 之后素邪, 我們需要重寫 onRequestPermissionsResult 方法,在
OnCreate() 外面. 如果用戶返回結(jié)果是 "accept"猪半,接著即可調(diào)用 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;
}
}
我在這里多寫了一句調(diào)用onClickStartNewActivity()方法來打開第二個(gè)Activity用以刷新主Activity (用戶許可會(huì)被系統(tǒng)記住兔朦,再打開主Activity時(shí) if 檢查就會(huì)通過,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);
}
第二個(gè)Activity就是說明界面. 較為簡單在這里就不細(xì)說了.
應(yīng)用語言自適應(yīng)
所有的系統(tǒng)String都放入資源文件夾內(nèi)的strings.xml內(nèi).
<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文件夾內(nèi)新建一個(gè)名為values-zh
的文件夾. 也放入一個(gè)strings.xml.
但這個(gè)文件設(shè)定為中文.
<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">藝術(shù)與文化</string>
<string name="club">酒吧與夜生活</string>
<string name="transport">交通樞紐</string>
<string name="hospital">醫(yī)院</string>
<string name="bank">銀行與貨幣兌換</string>
<string name="wc">廁所</string>
</resources>
這樣磨确,應(yīng)用就會(huì)根據(jù)系統(tǒng)設(shè)定的語言來自主選擇從哪一個(gè)資源文件內(nèi)選擇string了.
結(jié)尾處的BULABULA
現(xiàn)在的問題是沽甥,POI在小比例尺的視圖中都會(huì)消失不見,這個(gè)scale level能否改變一下乏奥,使得在小比例尺視圖中可以看到全城的概覽就是下一個(gè)需要考慮的問題啦~