Android 开发小作:Minofo(2)

本文作为 Minofo 开发的第二篇文章,详细介绍了 高德地图 API 的使用,包括地图 SDK 和定位 SDK 的用法,实现了 Minofo 的地图模块。另外还介绍了用车面板的实现以及利用 OkHttp 从服务器获取数据的方法,进而完成 Minofo 的开发。本文提供 PDF 版本可供查阅及下载。

Android 开发小作:Minofo(1)

Android 开发小作:Minofo(2)

通过我个人网站 Nightn 阅读此篇博客效果更佳哦!

本文接着上一篇博客:Android 开发小作:Minofo(1) ,继续记录 Minofo 的开发过程。在上一篇博客中,我们实现了主界面大部分功能的开发,包括标题栏、导航栏和悬浮按钮,实现效果如下:

在本篇博客中,将继续完善主界面,在主界面中添加地图模块。另外还有开发用车界面,用车界面还包括了整个软件的核心功能,即根据输入的车票号,获取对应密码。在此,我们在服务器端建立简单的模拟数据库,然后在 App 中通过网络库获取相应的密码,并显示在用车界面。让我们先从地图模块开始吧。

一、主界面嵌入高德地图

ofo 共享单车的地图模块具有显示当前区域地图、实时定位以及显示周边小黄车分布的功能,其中最后一个功能是 4 月份刚增加上去的,我不打算去实现(事实上,没有原始数据,想实现也实现不了)。因此,地图模块需要实现的功能包括地图显示和实时定位,这个比较简单,调用第三方地图 API 可以实现。纵观各大地图 API 产商:谷歌地图、百度地图、高德地图和腾讯地图等等。谷歌地图国内不能用,网上推荐百度地图和高德地图的比较多,而且郭霖老师在《第二行代码》中用的也是百度地图 API。因此一开始用百度地图 API,申请了 Key,下好了依赖库,但是出现了一堆问题,搜索了很久也没能解决,而且百度地图 API 文档也说得不清不楚,真的怀疑这些文档都是让实习生写的。折腾了一个晚上加一个上午还是没能解决,最终决定用高德地图的 API,下面总结一下使用高德地图 API 的流程吧。

1. 下载并安装高德地图开发包

首先需要下载并安装高德地图开发包,进入高德开放平台注册一个个人开发者用户。由于我们既需要地图功能有需要定位功能,因此我们要下载两个 SDK:Android 地图 SDKAndroid 定位 SDK。下载入口在首页的「开发与支持」菜单下面,如下图所示:

进入下载界面后,都选择「一键下载」,然后我们获得了两个 zip 压缩包,接下来从压缩包取出我们需要的东西。定位 SDK 比较简单,只需要定位的 jar 依赖库就可以了;地图 SDK 包含了很多功能: 2D 地图、3D 地图和搜索功能,我们只需要 3D 地图模块就可以了,3D 地图中除了包含 jar 包以外,还有很多针对不同平台的 so 文件,这些都是我们需要的。最后我们得到了两个 jar 包一堆 so 文件 ,如下图所示(当然如果你需要用到 2D 地图或者搜索功能,也可以将相应的开发包取出来)。

开发包都准备好了,现在将它们添加到项目里吧。将 jar 包复制到项目的 app/libs 目录。并在 app/src/main 目录下新建 jniLibs 文件夹,将之前提取出的 so 文件复制到 jniLibs 文件夹内,最终效果如下:

最后同步一下项目,使 Android Studio 意识到我们导入了新的包了,点击同步按钮即可:

2. 获取高德 Key

有了高德的 SDK 还是不够的,还需要获取高德 Key 并配置好项目才能真正的使用高德地图 API。进入高德控制台界面,创建新应用。

创建好应用之后,需要为它添加 Key,在添加 Key 界面,注意 PackageName 要与你项目的包名对应才行

另外还需要填写 App 发布版和调试版的安全码 SHA1,为了简单起见,我们在这里只去获取调试版的 SHA1,然后将两个 SHA1 都填写成调试版的 SHA1,如果之后想修改发布版的 SHA1 也是可以的(发布版的 SHA1 获取方法可以参考这里)。

下面开始获取调试版 SHA1,这个非常简单。在 Android Studio 界面右上角点击 Gradle,然后在跳出的 Gradle projects 面板中双击 :app/Tasks/android/signingReport,如下图所示:

然后在输出窗口便可以看到调试版的 SHA1 了,用这个 SHA1 便可以获得高德地图的 Key 了。

3. 配置 AndroidManifest.xml

最后配置好 AndroidManifest.xml 文件。首先进行权限声明


    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
    
    <uses-permission android:name="android.permission.INTERNET"/>
    
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"/>
    
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>

然后在 applicaiton 标签中设置高德 Key定位服务

<meta-data android:name="com.amap.api.v2.apikey" android:value="key">
//此处的 key 就是之前你自己申请的那个
meta-data>

<service android:name="com.amap.api.location.APSService">
service>

好了,至此高德地图开发包的配置就已经完成了,下面就可以在项目中正常使用了。

4. 使用高德地图

首先在主界面的布局文件中添加地图控件,打开 activity_main.xml,在之前定义的 FrameLayout 布局中加入如下代码:

<com.amap.api.maps.MapView
            android:id="@+id/map_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

这就是一个高德地图的地图显示控件 MapView,然后在 MainActivity.java 中定义一个名为 mapView 的 MapView 对象,在 onCreate 方法中通过 findViewById 找到布局中的 MapView 控件,并调用 mapView.onCreate() 方法初始化地图。

//高德地图
mapView = (MapView)findViewById(R.id.map_view);
//在activity执行onCreate时执行mMapView.onCreate(saveInsatenceState),创建地图
mapView.onCreate(savedInstanceState);

这样就成功在我们的 App 中添加并初始化了一个地图了,没错,就是这么简单。运行一下试试吧。注意!!引入高德地图之后如果还在模拟器里面调试,会出现闪退现象!这个问题我折腾了好久啊,一直以为是我自己的代码出了问题。因此在这里特别强调,引入高德地图之后,最好用真机调试。什么,你是 iPhone 用户,没有 Android 真机?那可能要尝试用不同的模拟器试试看了,有知道解决方案的同学可以在评论里和大家分享一下。我用的机型是荣耀 V8,项目完美运行:

由于只调用了一个 MapView.onCreate() 方法,因此创建出的地图默认是北京的地图,而且地图中定位蓝点也没有。为了让地图的功能更丰富,我们需要引入 AMap 类和 MyLocationStyle 类,AMap 类是地图控制器,用来管理地图的很多属性;而 MyLocationStyle 类是地图的定位风格属性,包括设置定位模式(连续定位还是单次定位)、连续定位的时间间隔、定位蓝点的样式等等。之后我们还要用到 UiSettings 类,它是用于控制地图界面小工具的显示隐藏的。为了可以更形象的理解高德地图中各个主要类是如何控制地图显示的,我将自己的理解表示成一张图,如下所示:

其中,AMap 是管理地图各种属性的主要控制器,但它并不是直接管理的,而是通过它的下级(注意,不是子类)来管理具体的属性,就像人类社会一样,在代码中也能体现出明确的分工。图上只画出了 AMap 管理的 MyLocationStyle 类和 UiSettings 类,其中 MyLocationStyle 类是管理定位风格的,如定位模式,连续定位间隔、定位蓝点的样式等;而 UiSettings 是控制地图上小工具的显示的,如缩放按钮、指南针、比例尺的显示与隐藏,高德地图 logo 的位置(这个 logo 是不能隐藏的)以及地图是否可以旋转等。既然明确了它们之间的逻辑关系,那么就开始编写代码吧,修改 MainActivity.java,先定义一个名为 aMap 类型为 AMap 的全局变量,在 onCreate() 方法中添加如下代码:

        //初始化地图控制器对象aMap
        if(aMap == null){
            aMap = mapView.getMap(); //将mapView交给地图控制器管理
        }

        MyLocationStyle myLocationStyle = new MyLocationStyle();
        myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATE); //设置定位模式
        //设置定位点的图标
      myLocationStyle.myLocationIcon(BitmapDescriptorFactory.fromResource(R.drawable.map_marker));
        //定义精度圆样式
        myLocationStyle.strokeColor(0);
        myLocationStyle.radiusFillColor(0);

        //将定位风格设置传给地图控制器
        aMap.setMyLocationStyle(myLocationStyle);
        aMap.setMyLocationEnabled(true);
        aMap.moveCamera(CameraUpdateFactory.zoomTo(17)); //设置缩放级别为17
        aMap.showIndoorMap(true); //显示室内地图  

首先判断 aMap 是否为空,如果为空则将地图显示控件 mapView 交由 aMap 管理。然后新建 MyLocationStyle 对象,设置定位模式为单次定位,之所以不设置成连续定位,是因为我们打算通过主界面左下角的那个定位刷新的悬浮按钮来刷新定位,如果设置成连续定位,那么我们不手动刷新,地图也会自己不停的定位,这是我们不愿意看到的,更是用户不愿意看到的(GPS 多费电啊~)。然后设置定位点的显示图标,由于 myLocationIcon 接收的是 BitmapDescriptor 类型的参数,因此这里不能直接传 R.drawable.map_marker,而是通过 BitmapDescriptorFactory 类的 fromResource 方法将其转换为 BitmapDescriptor 类型。之后的两条语句是设置精度圆的样式,什么是精度圆,这两句话有什么作用,看看下图你就知道了(下图是不加这两条语句时定位点的显示效果):

定位蓝点周围那一圈紫色的区域便是精度圆,默认是带有精度圆的。但是 ofo 共享单车的点位蓝点是不带精度圆的,因此我们就是通过上述两条语句,将精度圆的边框颜色和圆形区域颜色都设置为透明,以达到 ofo 共享单车上的效果。设置好 MyLocationStyle 的属性之后,将其加载到 aMap 中,并设置为允许显示。最后将地图缩放级别定义为 17 级,设置允许使用室内地图(室内地图很强大)。

经过上述设置之后如果直接运行的话,你会发现你被定位到非洲尼日利亚西南部某片神秘的不知名的大西洋海域(别问我是怎么知道的,说多了都是泪啊)。我猜想这是由于 app 的权限不够,虽然在 AndroidManifest.xml 中声明了权限,但有些权限进行运行时权限处理的,因此我们需要在 onCreate() 方法中加入一段申请权限的代码,只有用户同意了这些权限,才能继续操作。

//运行时权限
        List permissionList = new ArrayList<>();
        if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
                permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
            permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
        }
        if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
                permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED){
            permissionList.add(Manifest.permission.READ_PHONE_STATE);
        }
        if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
                permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
            permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }
        if(!permissionList.isEmpty()){
            String[] permissions = permissionList.toArray(new String[permissionList.size()]);
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
        }

现在我们再来运行一下,运行之后首先跳出申请权限对话框:

都点击允许之后便出现了主界面,此时可以看到,点位蓝点的位置就是你当前的定位,你可以移动、缩放地图,也可以查看室内地图。如下图所示,左图显示了我当前所处的位置,右图显示的是室内地图。

虽然地图模块已经显示成功了,而且可以准确定位,但是这实现了单次定位,如果我们移动了位置,想再次定位就要重启启动软件才行。因此,需要像 ofo 共享单车一样,点击左下角的刷新按钮便能够更新点位,下面这一节就来讲述怎么实现这一功能吧。

5. 实现点击定位

为了实现定位功能,我们需要使用两个类:AMapLocationClient 类和 AMapLocationClientOption 类。你可以把前者叫做定位客户端,后者叫做定位客户端设置,还需要一个 Marker 类,用于标记地图上的某个点。新建三个全局变量:

AMapLocationClient mLocationClient = null;
AMapLocationClientOption mLocationOption = null;
Marker locationMarker = null;

然后在 onCrate() 方法中添加如下代码:

mLocationClient = new AMapLocationClient(getApplicationContext());
        mLocationOption = new AMapLocationClientOption();
        mLocationOption.setOnceLocation(true); //设置为单次定位模式
        mLocationOption.setNeedAddress(true); //返回地址描述
        mLocationOption.setHttpTimeOut(10000); //设置请求超时时间
        mLocationClient.setLocationOption(mLocationOption);

        //设置定位回调监听器
        mLocationClient.setLocationListener(new AMapLocationListener() {
            @Override
            public void onLocationChanged(AMapLocation aMapLocation) {
                if(aMapLocation != null){
                    LatLng latLng = new LatLng(aMapLocation.getLatitude(), aMapLocation.getLongitude());
                    if(locationMarker == null){
                        locationMarker = aMap.addMarker(new MarkerOptions()
                                .position(latLng)
                                .icon(BitmapDescriptorFactory.fromResource(R.drawable.center_marker2)));
                    }else{
                        locationMarker.setPosition(latLng);
                    }
                    //将标记移动到定位点,使用animateCamera就有动画效果
                    aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 17));
                }else{
                    Toast.makeText(MainActivity.this, "定位失败", Toast.LENGTH_SHORT).show();
                }
            }
        });

程序的逻辑比较简单,注释都标明了语句的具体作用。需要注意的是这里为定位客户端注册了一个定位监听器,在监听器执行的代码便是获取当前经纬度,并用一个 marker 表示当前经纬度,显示在地图上。那么如何触发这个监听器呢?这个问题问得好,下面这条语句就可以触发:

mLocationClient.startLocation();

那么将这句代码放到哪里呢?当然是放到刷新按钮的点击事件的毁掉函数中呀,如下所示:

public void onClick(View v){
        switch(v.getId()){
            //...
            case R.id.refresh:{
                //TODO 刷新定位按钮点击事件
                mLocationClient.startLocation();
                break;
            }
            //...
        }
    }

这样一来,便实现了点击定位功能了。不过在此要注意两个细节。

(1)及时销毁对象

之前定义的 mapView 和 mLocationClient 都没有在主活动结束时进行销毁,因此我们要重写活动的一些回调方法,最后一个方法是保存地图缓存。

 @Override
    protected void onDestroy(){
        super.onDestroy();
        mapView.onDestroy();
        mLocationClient.onDestroy();//销毁定位客户端,同时销毁本地定位服务。
    }

    @Override
    protected void onResume(){
        super.onResume();
        mapView.onResume();
    }

    @Override
    protected void onPause(){
        super.onPause();
        mapView.onPause();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        mapView.onSaveInstanceState(outState);
    }

(2)调整地图 UI

在之前运行的结果中,可以发现地图界面右下角还保留着默认的缩放按钮,举报按钮就覆盖在它的上面,强迫症患者怎么能容忍这种事情发生呢,赶紧把缩放按钮隐藏掉。

怎么隐藏呢?还记得之前说过的 UiSettings 类吗,没错,就是用它来设置地图 UI 的。新建名为 mUiSettings 类型为 UiSettings 的对象,并在 onCreate() 中添加如下代码:

       //控件交互
        mUiSettings = aMap.getUiSettings();
        mUiSettings.setZoomControlsEnabled(false); //缩放按钮的显示与隐藏
        mUiSettings.setCompassEnabled(false); //指南针的显示与隐藏
        mUiSettings.setScaleControlsEnabled(false); //比例尺的显示与隐藏
        mUiSettings.setLogoPosition(AMapOptions.LOGO_POSITION_BOTTOM_LEFT); //       设置LOGO位置
        mUiSettings.setRotateGesturesEnabled(false); //禁止旋转

代码比较简单,就不多加解析了,看注释吧。好了,地图模块大功告成,点击刷新按钮便可实现更新定位功能了。

二、用车界面开发

1. Input 面板

地图功能实现之后,主界面的功能就已经都达到预期目标了,接下来开发用车界面。我们新建一个活动,叫作 UsebikeActivity,在生成的 activity_usebike.xml 中添加如下代码:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@drawable/bg_top">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/actionbar_logo"/>
        <ImageView
            android:id="@+id/saoma"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_marginRight="10dp"
            android:background="@drawable/saoma_logo"
            android:visibility="visible"/>
    android.support.v7.widget.Toolbar>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:background="@drawable/bg_usebike"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/bg_bike"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/bicycle_signal"/>

        <TextView
            android:id="@+id/hint"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_margin="10dp"
            android:background="@drawable/bg_text1"
            android:gravity="center_vertical"
            android:text="包学期期间(7.31前)用车免费喔"
            android:textColor="#40320D"/>

        <LinearLayout
            android:id="@+id/input_plate_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:gravity="center_vertical|center_horizontal"
            android:visibility="visible">

            <EditText
                android:id="@+id/code_edit"
                android:layout_width="240dp"
                android:layout_height="50dp"
                android:layout_margin="10dp"
                android:background="@drawable/bg_edit1"
                android:gravity="center_vertical|center_horizontal"
                android:inputType="number"
                android:textColor="#40320D"
                android:textSize="30sp"
                android:maxLength="8"/>

            <Button
                android:id="@+id/check_code"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_margin="5dp"
                android:background="@drawable/checkf"
                android:clickable="false"/>
        LinearLayout>

        <TextView
            android:id="@+id/code_hint"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginBottom="10dp"
            android:text="请输入车牌号,获取解锁码"
            android:textColor="#40320D" />
    LinearLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/light"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:src="@drawable/light_off"
        app:elevation="2dp"
        android:scaleType="center"
        android:visibility="visible"/> 
LinearLayout>

别看上面的代码有 100 行,其实这种 xml 文件是非常好理解的,不过是各种控件搭积木式的拼凑而已。简单分析一下,最外层是一个线性布局,将之前定义的 toolbar 标题栏直接拿过来用就好了,然后主体又是一个线性布局,里面就是 TextView、ImageView、EditView 和 Button 的简单组合而已,最底下的手电筒按钮是一个简单的 FloatingActionButton,悬浮按钮的用法在上一篇博客讲过。预览一下效果:

下面建立「主活动」和「用车活动」的联系,点击用车按钮,便跳出用车界面。实现很简单,用显式 Intent 即可,修改用车按钮的点击事件回调函数:

 public void onClick(View v){
        switch(v.getId()){
            case R.id.begin:{
                //TODO 开始按钮点击事件
                //启动 UsebikeActivity
                Intent intent = new Intent(MainActivity.this, UsebikeActivity.class);
                startActivity(intent);
                break;
            }
            //...
        }
    }

非常简单,两句代码搞定。这样通过点击主界面的「立即用车」,就能跳转到用车界面了,不过还有些细节要注意。

在用车界面中,EditText 内容有变化时,底下的 TextView 提示和右边的 Button 也要发生相应变化。在 UsebikeActivity 中为 codeEdit (就是车牌的输入框)注册内容变化事件的监听器。其中 checkCode 是输入框右边的 Button,codeHint 是输入框下方的 TextView,用于密码提示(原谅我拙计的命名方式,连我自己都搞不清了)。这些控件都需要事先通过 findViewById() 从布局中找到,这些过程就省略了。

//codeEdit输入发生变化的监听器
        codeEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                input = codeEdit.getText().toString();
                if(!input.isEmpty()){
                    checkCode.setClickable(true);
                    checkCode.setBackgroundResource(R.drawable.checkt);
                    if(input.length() < 4){
                        codeHint.setText("车牌号一般为4-8位的数字");
                    }else{
                        codeHint.setText("温馨提示:若输错车牌号,将无法打开车锁。");
                    }
                }else{
                    checkCode.setClickable(false);
                    checkCode.setBackgroundResource(R.drawable.checkf);
                }
            }
            @Override
            public void afterTextChanged(Editable s) {

            }
        });

在回调函数中,先判断输入框是否为空,如果不为空,则改变按钮的样式;然后判断输入框字符长度,根据长度给出相应提示,最大长度为 8 位,效果如下图所示:

2. Output 面板

用车界面的输入面板已经实现好了,下面要实现输入车牌之后,跳转到密码显示面板,即 Output 面板,这个面板要新建一个活动吗?当然不用,我们只要对 Input 面板进行适当修改,利用控件的 visibility 属性控制其可见性就能够实现了。通过在 activity_usebike.xml 中将输入框和按钮所对应的线性布局隐藏,以及隐藏小电筒按钮。用如下代码取而代之,即可实现 Output 面板。

<LinearLayout
            android:id="@+id/show_code_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:gravity="center_vertical|center_horizontal"
            android:visibility="visible">

            <TextView
                android:id="@+id/code1"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_margin="2dp"
                android:background="@drawable/bg_code"
                android:gravity="center_vertical|center_horizontal"
                android:text="4"
                android:textColor="#40320D"
                android:textSize="30sp"/>

            <TextView
                android:id="@+id/code2"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_margin="2dp"
                android:background="@drawable/bg_code"
                android:gravity="center_vertical|center_horizontal"
                android:text="2"
                android:textColor="#40320D"
                android:textSize="30sp" />

            <TextView
                android:id="@+id/code3"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_margin="2dp"
                android:background="@drawable/bg_code"
                android:gravity="center_vertical|center_horizontal"
                android:text="9"
                android:textColor="#40320D"
                android:textSize="30sp" />

            <TextView
                android:id="@+id/code4"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_margin="2dp"
                android:background="@drawable/bg_code"
                android:gravity="center_vertical|center_horizontal"
                android:text="2"
                android:textColor="#40320D"
                android:textSize="30sp" />
        LinearLayout>

预览效果如下:

三、用 OkHttp 获取服务器数据

用车界面制作完毕,下一步是根据用户在 Input 面板中输入的车牌,从服务器获取车牌对应的密码,然后显示在 Output 面板中。当然,这里的服务器可不是 ofo 共享单车的服务器,是我自己建的服务器,其实就是一个虚拟主机而已。可以事先在服务器端放置好模拟数据,这里我也不用什么 JSON 数据格式了,直接自定义数据格式为 `密码+:+车牌号+;。如下图所示:

然后在 UsebikeActivity 中为 codeCheck 按钮注册点击事件监听器,另外在类中定义一个 copy() 方法,用于将字符串复制到剪切板。

//确定按钮监听器
        checkCode.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                input = codeEdit.getText().toString();
                if(input.length() < 4){
                    Toast.makeText(UsebikeActivity.this, "请输入正确的车牌号!", Toast.LENGTH_SHORT).show();
                    return;
                }

                //TODO 访问远程数据库,检验是否匹配
                String url = ""; //TODO 此处填写服务器端url
                Utils.sendOkHttpRequest(url, new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        e.printStackTrace();
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        final String temp = response.body().string(); //这句 final 很重要
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                sql = temp;
                                //sql数据格式为:密码 + ":" + 车牌 + "分号"
                                index = sql.indexOf(":" + input);
                                if(index == -1){
                                    //复制到剪贴板
                                    copy(input, UsebikeActivity.this);
                                    //跳转至另一个APP的具体活动
                                    Intent intent = new Intent();
                                    Toast.makeText(UsebikeActivity.this, "已复制车牌号 " + input, Toast.LENGTH_SHORT).show();
                                    ComponentName cn = new ComponentName("so.ofo.labofo", "so.ofo.labofo.activities.EntryActivity");
                                    try {
                                        intent.setComponent(cn);
                                        intent.putExtra("车牌", input);
                                        startActivity(intent);
                                    } catch(Exception e) {
                                        //TODO  可以在这里提示用户没有安装应用或找不到指定Activity,或者是做其他的操作
                                        Toast.makeText(UsebikeActivity.this, "未安装 ofo 共享单车", Toast.LENGTH_SHORT).show();
                                        e.printStackTrace();
                                    }

                                }else{
                                    //TODO 这里验证 input 是否包含在云端数据库
                                    //获取并设置密码
                                    String code1 = String.valueOf(sql.charAt(index-4));
                                    String code2 = String.valueOf(sql.charAt(index-3));
                                    String code3 = String.valueOf(sql.charAt(index-2));
                                    String code4 = String.valueOf(sql.charAt(index-1));

                                    textCode1.setText(code1);
                                    textCode2.setText(code2);
                                    textCode3.setText(code3);
                                    textCode4.setText(code4);

                                    bgBike.setImageResource(R.drawable.unlock_bg_card);
                                    hint.setText("车牌号 " + input + " 的解锁码");
                                    hint.setTextSize(20);
                                    hint.setBackgroundResource(R.drawable.bg_white);
                                    codeHint.setText("骑行结束后,记得在手机上结束行程");
                                    inputLinear.setVisibility(View.GONE);
                                    showLinear.setVisibility(View.VISIBLE);
                                }
                            }
                        });
                    }
                });
            }
        });
//复制文字到剪贴板
    public static void copy(String content, Context context)
    {
        ClipboardManager clipboard = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
        ClipData clip = ClipData.newPlainText("label", content);
        clipboard.setPrimaryClip(clip);
    }

逻辑非常明了:若输入框中的字符个数小于 4,则跳出相应提示,并 return。否则,访问服务器获取数据,并在这些数据中寻找与输入车牌号相对应的密码,若找到,则显示到 Output 面板;若没有找到,则复制车牌号到剪贴板,并跳转至 ofo 共享单车(若手机上没有安装 ofo 共享单车,也会跳出相应提示)。最终实现效果:

视频预览效果

四、下载及声明

1 Apk 下载:点击下载 apk 或扫码下载:

2 Github 源码:https://github.com/nightn/minofo

3 声明:本产品用作分享与学习,若转载请注明出处,勿作任何商业用途。

你可能感兴趣的:(Android)