前 言
2016年底以来,国内共享单车突然就火爆了起来,而在街头,仿佛一夜之间,共享单车已经到了“泛滥”的地步,各大城市路边排满各种颜色的共享单车。
通过一番梳理发现,除了较早入局的摩拜单车、ofo外,整个2016年至少有25个新品牌汹涌入局,其中甚至还包括电动自行车共享品牌。
——来自百度百科
那么,既然共享单车那么火,那它们的App页面是怎样实现的,本博客将简单的介绍基于百度地图API与ZXing二维码开源库实现共享单车界面。
开发环境
Android Studio 3.1.2
JDK 1.8
开发前准备
- 在百度地图里申请个人密钥(详情请看我的另一篇CDSN博客——在Android项目里调用基于百度地图API实现定位);
- 在项目里导入 Google ZXing 二维码开源库作为依赖项(详情请看我的另一篇CDSN博客——在Android项目里集成开源框架ZXing实现扫描二维码的功能)。
编码与实现
- 共享单车主界面布局activity_main.xml文件代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.baidu.mapapi.map.MapView
android:id="@+id/id_bmapView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:paddingLeft="10dp"
android:src="@drawable/icon_bike" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:paddingLeft="3dp"
android:text="共享单车"
android:textColor="#FF4500"
android:textSize="20sp" />
</LinearLayout>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:onClick="onPopupMenuClick"
android:text="地图模式"
android:textSize="16sp" />
</RelativeLayout>
</LinearLayout>
<ImageButton
android:id="@+id/id_bn_getMyLocation"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="60dp" />
<Button
android:id="@+id/sweep_QRcode"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="50dp"
android:background="@mipmap/button_bg"
android:textSize="20sp" />
</RelativeLayout>
- 共享单车主界面逻辑MainActivity.java文件核心代码如下:
public class MainActivity extends Activity {
private static final int BAIDU_READ_PHONE_STATE = 100;
private MapView mMapView = null;
private BaiduMap mBaiduMap;
private LocationClient mlocationClient;
private MylocationListener mlistener;
private Context context;
private Button mQRcode;
private double mLatitude;
private double mLongitude;
private float mCurrentX;
private ImageButton mGetMylocationBN;
PopupMenu popup = null;
//自定义图标
private BitmapDescriptor mIconLocation;
private MyOrientationListener myOrientationListener;
//定位图层显示方式
private MyLocationConfiguration.LocationMode locationMode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SDKInitializer.initialize(getApplicationContext());
setContentView(R.layout.activity_main);
this.context = this;
initView();
//判断是否为Android 6.0 以上的系统版本,如果是,需要动态添加权限
if (Build.VERSION.SDK_INT >= 23) {
showContacts();
} else {
initLocation();//initLocation为定位方法
}
}
private void initView() {
mMapView = (MapView) findViewById(R.id.id_bmapView);
mBaiduMap = mMapView.getMap();
//根据给定增量缩放地图级别
MapStatusUpdate msu = MapStatusUpdateFactory.zoomTo(18.0f);
mBaiduMap.setMapStatus(msu);
MapStatus mMapStatus;//地图当前状态
MapStatusUpdate mMapStatusUpdate;//地图将要变化成的状态
mMapStatus = new MapStatus.Builder().overlook(-45).build();
mMapStatusUpdate = MapStatusUpdateFactory.newMapStatus(mMapStatus);
mBaiduMap.setMapStatus(mMapStatusUpdate);
mGetMylocationBN = (ImageButton) findViewById(R.id.id_bn_getMyLocation);
mGetMylocationBN.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getMyLocation();
}
});
mQRcode = (Button) findViewById(R.id.sweep_QRcode);
mQRcode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, FindScanActivity.class);
startActivity(intent);
}
});
}
/**
* 定位方法
*/
private void initLocation() {
locationMode = MyLocationConfiguration.LocationMode.NORMAL;
//定位服务的客户端。宿主程序在客户端声明此类,并调用,目前只支持在主线程中启动
mlocationClient = new LocationClient(this);
mlistener = new MylocationListener();
//注册监听器
mlocationClient.registerLocationListener(mlistener);
//配置定位SDK各配置参数,比如定位模式、定位时间间隔、坐标系类型等
LocationClientOption mOption = new LocationClientOption();
//设置坐标类型
mOption.setCoorType("bd09ll");
//设置是否需要地址信息,默认为无地址
mOption.setIsNeedAddress(true);
//设置是否打开gps进行定位
mOption.setOpenGps(true);
//设置扫描间隔,单位是毫秒,当<1000(1s)时,定时定位无效
int span = 1000;
mOption.setScanSpan(span);
//设置 LocationClientOption
mlocationClient.setLocOption(mOption);
//初始化图标,BitmapDescriptorFactory是bitmap 描述信息工厂类.
mIconLocation = BitmapDescriptorFactory
.fromResource(R.mipmap.icon_geo);
myOrientationListener = new MyOrientationListener(context);
//通过接口回调来实现实时方向的改变
myOrientationListener.setOnOrientationListener(new MyOrientationListener.OnOrientationListener() {
@Override
public void onOrientationChanged(float x) {
mCurrentX = x;
}
});
}
@Override
protected void onStart() {
super.onStart();
//开启定位
mBaiduMap.setMyLocationEnabled(true);
if (!mlocationClient.isStarted()) {
mlocationClient.start();
}
myOrientationListener.start();
}
@Override
protected void onStop() {
super.onStop();
//停止定位
mBaiduMap.setMyLocationEnabled(false);
mlocationClient.stop();
myOrientationListener.stop();
}
@Override
protected void onResume() {
super.onResume();
mMapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mMapView.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
mMapView.onDestroy();
}
public void getMyLocation() {
LatLng latLng = new LatLng(mLatitude, mLongitude);
MapStatusUpdate msu = MapStatusUpdateFactory.newLatLng(latLng);
mBaiduMap.setMapStatus(msu);
}
public void onPopupMenuClick(View v) {
// 创建PopupMenu对象
popup = new PopupMenu(this, v);
// 将R.menu.menu_main菜单资源加载到popup菜单中
getMenuInflater().inflate(R.menu.menu_main, popup.getMenu());
// 为popup菜单的菜单项单击事件绑定事件监听器
popup.setOnMenuItemClickListener(
new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.id_map_common:
mBaiduMap.setMapType(BaiduMap.MAP_TYPE_NORMAL);
break;
case R.id.id_map_site:
mBaiduMap.setMapType(BaiduMap.MAP_TYPE_SATELLITE);
break;
case R.id.id_map_traffic:
if (mBaiduMap.isTrafficEnabled()) {
mBaiduMap.setTrafficEnabled(false);
item.setTitle("实时交通(off)");
} else {
mBaiduMap.setTrafficEnabled(true);
item.setTitle("实时交通(on)");
}
break;
case R.id.id_map_mlocation:
getMyLocation();
break;
case R.id.id_map_model_common:
//普通模式
locationMode = MyLocationConfiguration.LocationMode.NORMAL;
break;
case R.id.id_map_model_following:
//跟随模式
locationMode = MyLocationConfiguration.LocationMode.FOLLOWING;
break;
case R.id.id_map_model_compass:
//罗盘模式
locationMode = MyLocationConfiguration.LocationMode.COMPASS;
break;
}
return true;
}
});
popup.show();
}
/**
* 所有的定位信息都通过接口回调来实现
*/
public class MylocationListener implements BDLocationListener {
//定位请求回调接口
private boolean isFirstIn = true;
//定位请求回调函数,这里面会得到定位信息
@Override
public void onReceiveLocation(BDLocation bdLocation) {
//BDLocation 回调的百度坐标类,内部封装了如经纬度、半径等属性信息
//MyLocationData 定位数据,定位数据建造器
/**
* 可以通过BDLocation配置如下参数
* 1.accuracy 定位精度
* 2.latitude 百度纬度坐标
* 3.longitude 百度经度坐标
* 4.satellitesNum GPS定位时卫星数目 getSatelliteNumber() gps定位结果时,获取gps锁定用的卫星数
* 5.speed GPS定位时速度 getSpeed()获取速度,仅gps定位结果时有速度信息,单位公里/小时,默认值0.0f
* 6.direction GPS定位时方向角度
* */
mLatitude = bdLocation.getLatitude();
mLongitude = bdLocation.getLongitude();
MyLocationData data = new MyLocationData.Builder()
.direction(mCurrentX)//设定图标方向
.accuracy(bdLocation.getRadius())//getRadius 获取定位精度,默认值0.0f
.latitude(mLatitude)//百度纬度坐标
.longitude(mLongitude)//百度经度坐标
.build();
//设置定位数据, 只有先允许定位图层后设置数据才会生效,参见 setMyLocationEnabled(boolean)
mBaiduMap.setMyLocationData(data);
//配置定位图层显示方式,三个参数的构造器
/**
* 1.定位图层显示模式
* 2.是否允许显示方向信息
* 3.用户自定义定位图标
* */
MyLocationConfiguration configuration
= new MyLocationConfiguration(locationMode, true, mIconLocation);
//设置定位图层配置信息,只有先允许定位图层后设置定位图层配置信息才会生效,参见 setMyLocationEnabled(boolean)
mBaiduMap.setMyLocationConfigeration(configuration);
//判断是否为第一次定位,是的话需要定位到用户当前位置
if (isFirstIn) {
//地理坐标基本数据结构
LatLng latLng = new LatLng(bdLocation.getLatitude(), bdLocation.getLongitude());
//描述地图状态将要发生的变化,通过当前经纬度来使地图显示到该位置
MapStatusUpdate msu = MapStatusUpdateFactory.newLatLng(latLng);
//改变地图状态
mBaiduMap.setMapStatus(msu);
isFirstIn = false;
Utils.showToast2(context, "亲,您当前的位置为:" + bdLocation.getAddrStr());
//定义Maker坐标点
LatLng point = new LatLng(mLatitude + 0.00056, mLongitude);
//构建Marker图标
BitmapDescriptor bitmap = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option = new MarkerOptions()
.position(point)
.icon(bitmap);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option);
//定义Maker坐标点
LatLng point1 = new LatLng(mLatitude, mLongitude + 0.0003);
//构建Marker图标
BitmapDescriptor bitmap1 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option1 = new MarkerOptions()
.position(point1)
.icon(bitmap1);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option1);
//定义Maker坐标点
LatLng point2 = new LatLng(mLatitude + 0.0005, mLongitude + 0.001);
//构建Marker图标
BitmapDescriptor bitmap2 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option2 = new MarkerOptions()
.position(point2)
.icon(bitmap2);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option2);
//定义Maker坐标点
LatLng point3 = new LatLng(mLatitude + 0.0003, mLongitude + 0.0001);
//构建Marker图标
BitmapDescriptor bitmap3 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option3 = new MarkerOptions()
.position(point3)
.icon(bitmap3);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option3);
//定义Maker坐标点
LatLng point4 = new LatLng(mLatitude + 0.0006, mLongitude);
//构建Marker图标
BitmapDescriptor bitmap4 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option4 = new MarkerOptions()
.position(point4)
.icon(bitmap4);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option4);
//定义Maker坐标点
LatLng point5 = new LatLng(mLatitude, mLongitude + 0.0004);
//构建Marker图标
BitmapDescriptor bitmap5 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option5 = new MarkerOptions()
.position(point5)
.icon(bitmap5);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option5);
//定义Maker坐标点
LatLng point6 = new LatLng(mLatitude, mLongitude - 0.0008);
//构建Marker图标
BitmapDescriptor bitmap6 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option6 = new MarkerOptions()
.position(point6)
.icon(bitmap6);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option1);
//定义Maker坐标点
LatLng point7 = new LatLng(mLatitude - 0.0005, mLongitude + 0.0004);
//构建Marker图标
BitmapDescriptor bitmap7 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option7 = new MarkerOptions()
.position(point7)
.icon(bitmap7);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option7);
//定义Maker坐标点
LatLng point8 = new LatLng(mLatitude - 0.0003, mLongitude - 0.0005);
//构建Marker图标
BitmapDescriptor bitmap8 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option8 = new MarkerOptions()
.position(point8)
.icon(bitmap8);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option8);
//定义Maker坐标点
LatLng point9 = new LatLng(mLatitude - 0.0003, mLongitude);
//构建Marker图标
BitmapDescriptor bitmap9 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option9 = new MarkerOptions()
.position(point9)
.icon(bitmap9);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option9);
//定义Maker坐标点
LatLng point10 = new LatLng(mLatitude, mLongitude - 0.0003);
//构建Marker图标
BitmapDescriptor bitmap10 = BitmapDescriptorFactory
.fromResource(R.drawable.car_small);
//构建MarkerOption,用于在地图上添加Marker
OverlayOptions option10 = new MarkerOptions()
.position(point10)
.icon(bitmap10);
//在地图上添加Marker,并显示
mBaiduMap.addOverlay(option10);
}
}
}
/**
* Android 6.0 以上的版本的定位方法
*/
public void showContacts() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
Toast.makeText(getApplicationContext(), "没有权限,请手动开启定位权限", Toast.LENGTH_SHORT).show();
// 申请一个(或多个)权限,并提供用于回调返回的获取码(用户定义)
ActivityCompat.requestPermissions(MainActivity.this, new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_PHONE_STATE
}, BAIDU_READ_PHONE_STATE);
} else {
initLocation();
}
}
//Android 6.0 以上的版本申请权限的回调方法
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
// requestCode即所声明的权限获取码,在checkSelfPermission时传入
case BAIDU_READ_PHONE_STATE:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 获取到权限,作相应处理(调用定位SDK应当确保相关权限均被授权,否则可能引起定位失败)
initLocation();
} else {
// 没有获取到权限,做特殊处理
Toast.makeText(getApplicationContext(), "获取位置权限失败,请手动开启", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
}
- 共享单车扫描界面activity_find_scan.xml布局文件代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/sv_scan"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<com.fukaimei.scanqrcodetest.zxing.view.ViewfinderView
android:id="@+id/vv_finder"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@drawable/bg_alpha"
android:gravity="center"
android:text="扫码开锁"
android:textColor="#ffffffff"
android:textSize="20sp" />
</RelativeLayout>
- 共享单车扫描界面逻辑FindScanActivity.java文件核心代码如下:
public class FindScanActivity extends Activity implements SurfaceHolder.Callback {
private final static String TAG = "FindScanActivity";
private CaptureActivityHandler mHandler;
private ViewfinderView vv_finder;
private boolean hasSurface = false;
private InactivityTimer mTimer;
private MediaPlayer mPlayer;
private boolean bBeep;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_find_scan);
// 获取相机的动态权限
cameraPermissions();
CameraManager.init(getApplication(), CameraManager.QR_CODE);
vv_finder = (ViewfinderView) findViewById(R.id.vv_finder);
mTimer = new InactivityTimer(this);
}
// 定义获取相机的动态权限
private void cameraPermissions() {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{
android.Manifest.permission.CAMERA}, 1);
}
}
/**
* 重写onRequestPermissionsResult方法
* 获取动态权限请求的结果,再开启相机扫码
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
CameraManager.init(getApplication(), CameraManager.QR_CODE);
} else {
Toast.makeText(this, "用户拒绝了权限", Toast.LENGTH_SHORT).show();
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onResume() {
super.onResume();
SurfaceView sv_scan = (SurfaceView) findViewById(R.id.sv_scan);
SurfaceHolder surfaceHolder = sv_scan.getHolder();
if (hasSurface) {
initCamera(surfaceHolder);
} else {
surfaceHolder.addCallback(this);
}
bBeep = true;
AudioManager audioService = (AudioManager) getSystemService(AUDIO_SERVICE);
if (audioService.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
bBeep = false;
}
initBeepSound();
}
@Override
protected void onPause() {
super.onPause();
if (mHandler != null) {
mHandler.quitSynchronously();
mHandler = null;
}
CameraManager.get().closeDriver();
}
@Override
protected void onDestroy() {
mTimer.shutdown();
super.onDestroy();
}
public void handleDecode(Result result, Bitmap barcode) {
mTimer.onActivity();
beepAndVibrate();
String resultString = result.getText();
if (resultString == null || resultString.length() <= 0) {
Toast.makeText(this, "Scan failed or result is null", Toast.LENGTH_SHORT).show();
} else {
String desc = String.format("barcode width=%d,height=%d",
barcode.getWidth(), barcode.getHeight());
// Toast.makeText(this, desc, Toast.LENGTH_SHORT).show();
Intent intent = new Intent(this, ScanResultActivity.class);
intent.putExtra("result", resultString);
startActivity(intent);
}
}
private void initCamera(SurfaceHolder surfaceHolder) {
try {
CameraManager.get().openDriver(surfaceHolder);
if (mHandler == null) {
mHandler = new CaptureActivityHandler(this, null, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!hasSurface) {
hasSurface = true;
initCamera(holder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
hasSurface = false;
}
public ViewfinderView getViewfinderView() {
return vv_finder;
}
public Handler getHandler() {
return mHandler;
}
public void drawViewfinder() {
vv_finder.drawViewfinder();
}
private void initBeepSound() {
if (bBeep && mPlayer == null) {
setVolumeControlStream(AudioManager.STREAM_MUSIC);
mPlayer = new MediaPlayer();
mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mPlayer.setOnCompletionListener(beepListener);
AssetFileDescriptor file = getResources().openRawResourceFd(R.raw.beep);
try {
mPlayer.setDataSource(file.getFileDescriptor(),
file.getStartOffset(), file.getLength());
file.close();
mPlayer.setVolume(0.1f, 0.1f);
mPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
mPlayer = null;
}
}
}
private static final long VIBRATE_DURATION = 200L;
private void beepAndVibrate() {
if (bBeep && mPlayer != null) {
mPlayer.start();
}
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(VIBRATE_DURATION);
}
private final OnCompletionListener beepListener = new OnCompletionListener() {
public void onCompletion(MediaPlayer mPlayer) {
mPlayer.seekTo(0);
}
};
}
测试与运行截图
APK下载
apk Demo下载体验地址如下: