安卓穿戴项目(三)
原文:
zh.annas-archive.org/md5/C54AD11200D923EEEED7A76F711AA69C译者:飞龙
第五章:测量您的健康状态并同步收集的传感器数据
在上一章中,我们构建了一个提醒我们喝水的 Wear 应用,并通过内置的 Wear 传感器检查步数和心率。Wear 和移动应用的理念是提高浏览的便捷性,不错过任何重要信息,而 Upbeat 项目看似功能不多,但在用户的腕上和口袋中占有一席之地。目前 upbeat Wear 应用的功能仅限于显示从传感器接收到的数据。在本章中,我们将通过 Wear 和移动应用的互操作性来增强这个应用。我们将通过RealmDB持久化所有传输的数据。我们将从移动设备向 Wear 发送通知,启动应用以检查心率。我们将在 Wear 应用中拥有健康提示和食物卡路里卡片列表。
在本章中,我们将探讨以下内容:
-
收集 Wear 传感器的数据
-
处理接收到的数据以查找卡路里和距离
-
WearableListenerService和消息传递 API -
从移动应用向 Wear 应用发送数据
-
RealmDB集成 -
使用
CardView的WearableRecyclerview
收集 Wear 传感器的数据
从 Wear 设备收集传感器数据需要一个通信机制,而 Wearable DataLayer API 作为 Google Play 服务的一部分,在通信过程中扮演着重要角色。我们将在后续课程中深入探讨通信过程,但本章我们需要在移动应用中接收传感器数据。我们已经创建了一个项目,其中已经包含移动模块和简单的“Hello World”样板代码。当我们从 Wear 应用设置数据发送机制后,我们将处理移动模块。让我们从 Wear 模块的服务包中的步数传感器开始,进入WearStepService类。我们已经构建了这个服务,用于发送通知并监听步数计数器数据。现在,在GoogleApiClient和 Wear 消息传递 API 的帮助下,我们需要将数据发送到移动应用。
在WearStepService类中,在类的全局范围内实例化GoogleApiClient:
GoogleApiClient mGoogleApiClient;
在onStartCommand中,调用初始化mGoogleApiClient的方法:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand");
getSensorManager();
getCountSensor();
getGoogleClient();
return super.onStartCommand(intent, flags, startId);
}
为了初始化GoogleClient,我们将使用GoogleClient的构建器模式,并需要添加Wearable.API。然后,我们可以使用connect()方法后跟构建器的build()方法连接GoogleClient:
private void getGoogleClient() {
if (null != mGoogleApiClient)
return;
Log.d(TAG, "getGoogleClient");
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.build();
mGoogleApiClient.connect();
}
在WearStepService类中,我们将重写属于IBinder接口的onBind方法。我们可以通过以下方式使用它进行远程服务的客户端交互:
@Override
public IBinder onBind(Intent intent) {
return null;
}
我们返回 null,因为发送数据后我们不希望有任何返回。如果我们希望返回某些信息,那么我们可以按以下方式返回IBinder实例:
private final IBinder mBinder = new LocalBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
为了能够向移动设备发送数据,我们需要两个 DataLayer API 机制,即可穿戴节点和消息 API。我们将使用 Node API 获取连接的节点。使用消息 API,我们将数据发送到特定路径,而在接收端,我们应该监听该路径以获取数据。
在 Node API 中,我们将有Resultcallback类,它返回ConnectedNodes的列表,我们必须实现具有返回连接节点列表能力的onResult方法。我们可以向所有连接的节点发送消息,或者只向已连接的节点发送。我们可以使用节点类的getDisplayname获取连接节点的名称,如下所示:
node.getDisplayName();
目前,我们将使用 Node 和 Message API 并将数据发送到连接的节点:
private void sendData(){
if (mGoogleApiClient == null)
return;
// use the api client to send the heartbeat value to our handheld
final PendingResult<NodeApi.GetConnectedNodesResult> nodes =
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
nodes.setResultCallback(new
ResultCallback<NodeApi.GetConnectedNodesResult>() {
@Override
public void onResult(NodeApi.GetConnectedNodesResult result) {
final List<Node> nodes = result.getNodes();
final String path = "/stepcount";
String Message = StepsTaken.getSteps()+"";
for (Node node : nodes) {
Log.d(TAG, "SEND MESSAGE TO HANDHELD: " + Message);
node.getDisplayName();
byte[] data = Message.getBytes(StandardCharsets.UTF_8);
Wearable.MessageApi.sendMessage(mGoogleApiClient,
node.getId(), path, data);
}
}
});
}
在上一个方法中,我们将使用PendingResults类来获取连接节点的结果。在我们收到连接节点的列表后,我们可以使用wearableMessageApi类发送消息。别忘了发送和接收数据到相同的路径。
完成的 WearStepService 类
完整的WearStepService类代码如下:
public class WearStepService extends Service implements SensorEventListener {
public static final String TAG = "WearStepService";
private static final long THREE_MINUTES = 3 * 60 * 1000;
private static final String STEP_COUNT_PATH = "/step-count";
private static final String STEP_COUNT_KEY = "step-count";
private SensorManager sensorManager;
private Sensor countSensor;
GoogleApiClient mGoogleApiClient;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
setAlarm();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand");
getSensorManager();
getCountSensor();
getGoogleClient();
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void getGoogleClient() {
if (null != mGoogleApiClient)
return;
Log.d(TAG, "getGoogleClient");
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.build();
mGoogleApiClient.connect();
}
/**
* if the countSensor is null, try initializing it, and try
registering it with sensorManager
*/
private void getCountSensor() {
if (null != countSensor)
return;
Log.d(TAG, "getCountSensor");
countSensor =
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
registerCountSensor();
}
/**
* if the countSensor exists, then try registering
*/
private void registerCountSensor() {
if (countSensor == null)
return;
Log.d(TAG, "sensorManager.registerListener");
sensorManager.registerListener(this, countSensor,
SensorManager.SENSOR_DELAY_UI);
}
/**
* if the sensorManager is null, initialize it, and try registering
the countSensor
*/
private void getSensorManager() {
if (null != sensorManager)
return;
Log.d(TAG, "getSensorManager");
sensorManager = (SensorManager)
getSystemService(Context.SENSOR_SERVICE);
registerCountSensor();
}
private void setAlarm() {
Log.d(TAG, "setAlarm");
Intent intent = new Intent(this, AlarmNotification.class);
PendingIntent pendingIntent =
PendingIntent.getBroadcast(this.getApplicationContext(),
234324243, intent, 0);
AlarmManager alarmManager = (AlarmManager)
getSystemService(ALARM_SERVICE);
long firstRun = System.currentTimeMillis() + THREE_MINUTES;
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP,
firstRun, THREE_MINUTES, pendingIntent);
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_STEP_COUNTER)
StepsTaken.updateSteps(event.values.length);
Log.d(TAG, "onSensorChanged: steps count is" +
event.values.length);
// sendToPhone();
sendData();
updateNotification();
}
private void sendData(){
if (mGoogleApiClient == null)
return;
// use the api client to send the heartbeat value to our
handheld
final PendingResult<NodeApi.GetConnectedNodesResult> nodes =
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
nodes.setResultCallback(new
ResultCallback<NodeApi.GetConnectedNodesResult>() {
@Override
public void onResult(NodeApi.GetConnectedNodesResult
result) {
final List<Node> nodes = result.getNodes();
final String path = "/stepcount";
String Message = StepsTaken.getSteps()+"";
for (Node node : nodes) {
Log.d(TAG, "SEND MESSAGE TO HANDHELD: " + Message);
node.getDisplayName();
byte[] data =
Message.getBytes(StandardCharsets.UTF_8);
Wearable.MessageApi.sendMessage(mGoogleApiClient,
node.getId(), path, data);
}
}
});
}
private void updateNotification() {
// Create a notification builder that's compatible with
platforms >= version 4
NotificationCompat.Builder builder =
new NotificationCompat.Builder
(getApplicationContext());
// Set the title, text, and icon
builder.setContentTitle(getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_step_icon);
builder.setContentText("steps: " + StepsTaken.getSteps());
// Get an instance of the Notification Manager
NotificationManager notifyManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
// Build the notification and post it
notifyManager.notify(0, builder.build());
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// drop these messages
updateNotification();
}
}
我们成功地向提到的路径发送了消息。现在,让我们看看如何从 Wear 设备接收消息。在移动模块内部,为代码可读性创建额外的包。我们将把包命名为 models、services 和 utils。
是时候创建一个扩展了WearableListenerService并覆盖了onMessageReceived方法的类了。创建一个名为StepListener的类,并让它扩展WearableListenerService;代码如下:
public class StepListner extends WearableListenerService {
private static final String TAG = "StepListner";
@Override
public void onMessageReceived(MessageEvent messageEvent) {
if (messageEvent.getPath().equals("/stepcount")) {
final String message = new String(messageEvent.getData());
Log.v(TAG, "Message path received from wear is: " +
messageEvent.getPath());
Log.v(TAG, "Message received on watch is: " + message);
// Broadcast message to wearable activity for display
Intent messageIntent = new Intent();
messageIntent.setAction(Intent.ACTION_SEND);
messageIntent.putExtra("message", message);
LocalBroadcastManager.getInstance(this)
.sendBroadcast(messageIntent);
}
else {
super.onMessageReceived(messageEvent);
}
}
}
在清单文件中使用与发送数据相同的路径注册之前的服务类,以下代码说明了可穿戴设备的DATA_CHANGED和MESSAGE_RECEIVED动作以及数据路径:
<service android:name=".services.StepListner">
<intent-filter>
<action android:name=
"com.google.android.gms.wearable.DATA_CHANGED" />
<action android:name=
"com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/stepcount"
android:scheme="wear" />
</intent-filter>
</service>
Steplistner类已完成;我们可以使用这个类进一步处理数据。在steplistener类中,我们注册了localbroadcast接收器类,以在广播接收器的作用域内发送接收到的数据。在我们构建用户界面之前,我们将在移动应用程序的MainActivity中接收所有数据。编写一个内部类以读取收到的步骤:
public class StepReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String message = intent.getStringExtra("message");
Log.v("steps", "Main activity received message: " +
message);
// Shows the step counts received by the wearlistener //service
mSteps.setText("Steps:"+ message);
int value = Integer.valueOf(message);
}
}
使用以下代码在oncreate方法中注册该类:
// Register the local broadcast receiver
IntentFilter StepFilter = new IntentFilter(Intent.ACTION_SEND);
StepReceiver StepReceiver = new StepReceiver();
LocalBroadcastManager.getInstance(this).registerReceiver(StepReceiver, StepFilter);
我们成功收集了步数计数器数据。让我们用同样的过程来收集脉搏速率。我们将持久化步数计数,稍后,我们将实时传输连接节点的心跳脉搏速率的实时流。
切换回 Wear 模块
将项目范围切换到 Wear 模块,并选择HeartRateFragment以实例化GoogleClient对象:
private GoogleApiClient mGoogleApiClient;
在oncreate方法中初始化GoogleClient实例,如下所示:
mGoogleApiClient = new GoogleApiClient.Builder(getActivity()).
addApi(Wearable.API).
build();
mGoogleApiClient.connect();
编写一个将脉搏速率计数发送到连接节点的方法,就像我们之前为步数计数器所做的那样:
private void sendMessageToHandheld(final String message) {
if (mGoogleApiClient == null)
return;
// use the api client to send the heartbeat value to our handheld
final PendingResult<NodeApi.GetConnectedNodesResult> nodes =
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
nodes.setResultCallback(new
ResultCallback<NodeApi.GetConnectedNodesResult>() {
@Override
public void onResult(NodeApi.GetConnectedNodesResult result) {
final List<Node> nodes = result.getNodes();
final String path = "/heartRate";
for (Node node : nodes) {
Log.d(TAG, "SEND MESSAGE TO HANDHELD: " + message);
byte[] data = message.getBytes(StandardCharsets.UTF_8);
Wearable.MessageApi.sendMessage(mGoogleApiClient,
node.getId(), path, data);
}
}
});
}
在onSensorchanged回调内部调用方法,并从传感器事件触发中接收 BPM 计数:
sendMessageToHandheld(currentValue.toString());
切换到移动项目范围。我们需要一个WearableListenerService类来与心率数据进行通信:
public class HeartListener extends WearableListenerService {
@Override
public void onMessageReceived(MessageEvent messageEvent) {
}
}
在onMessageReceived回调中注册一个localbroadcast事件,以在活动中接收数据。完整的监听器类代码如下:
public class HeartListener extends WearableListenerService {
@Override
public void onMessageReceived(MessageEvent messageEvent) {
if (messageEvent.getPath().equals("/heartRate")) {
final String message = new String(messageEvent.getData());
Log.v("pactchat", "Message path received on watch is: " +
messageEvent.getPath());
Log.v("packtchat", "Message received on watch is: " +
message);
// Broadcast message to wearable activity for display
Intent messageIntent = new Intent();
messageIntent.setAction(Intent.ACTION_SEND);
messageIntent.putExtra("heart", message);
LocalBroadcastManager.getInstance(this)
.sendBroadcast(messageIntent);
}
else {
super.onMessageReceived(messageEvent);
}
}
}
在 Manifest 中注册service类如下:
<service android:name=".services.HeartListener">
<intent-filter>
<action android:name=
"com.google.android.gms.wearable.DATA_CHANGED" />
<action android:name=
"com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/heartRate"
android:scheme="wear" />
</intent-filter>
</service>
在MainActivity中,我们将编写另一个广播接收器。我们称之为HeartRateReceiver:
public class HeartRateReciver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String data = intent.getStringExtra("heart");
Log.v("heart", "Main activity received message: " +
message);
mHeart.setText(message);
}
}
在oncreate方法中如下注册BroadcastReceiver:
// Register the local broadcast receiver
IntentFilter DataFilter = new IntentFilter(Intent.ACTION_SEND);
HeartRateReciver DataReceiver = new HeartRateReciver();
LocalBroadcastManager.getInstance(this).registerReceiver(DataReceiver, DataFilter);
我们已经成功从HeartListener直接接收心率数据到broadcastreceiver。现在,让我们处理移动项目的用户界面。我们需要保持 UI 简单而强大;以下设计涉及到与 Wear 应用的互操作性以及距离和卡路里消耗预测。
概念化应用程序
Upbeat 移动应用程序应显示步数和心率。Upbeat 需要向 Wear 应用发送心率请求。历史记录显示从数据库中消耗的距离和卡路里。重置将清除数据库。
着陆页:当用户打开应用时,他将看到类似于以下设计的内容:
在开始设计之前,我们需要确定一些事情,比如颜色、背景等。在res/values目录中,打开colors.xml文件,并添加以下颜色值:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#607d8b</color>
<color name="colorPrimaryDark">#34515e</color>
<color name="colorAccent">#FFF</color>
<color name="grey">#afaeae</color>
<color name="white">#fff</color>
</resources>
创建一个名为button_bg.xml的drawable资源文件,并添加以下选择器代码以选择背景:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/colorPrimaryDark"
android:state_pressed="true"/>
<item android:drawable="@color/grey" android:state_focused="true"/>
<item android:drawable="@color/colorPrimary"/>
</selector>
在activity_main.xml中,根据设计和计划的功能,我们需要三个按钮和三个文本视图。我们将使用相对布局作为根容器,以下代码解释了如何操作:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.packt.upbeat.MainActivity">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="102dp"
android:layout_above="@+id/calory"
android:layout_centerHorizontal="true">
<TextView
android:id="@+id/steps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/linearLayout"
android:layout_toStartOf="@+id/calory"
android:layout_weight="1"
android:text="Steps!"
android:textColor="@color/colorPrimaryDark"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_weight="1"
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<ImageView
android:id="@+id/heartbeat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/heart"
android:layout_below="@+id/heart"/>
<TextView
android:id="@+id/heart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/steps"
android:layout_alignBottom="@+id/steps"
android:layout_alignParentEnd="true"
android:layout_marginEnd="25dp"
android:text="Heart!"
android:textColor="@color/colorPrimaryDark"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/calory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Calories!"
android:textColor="@color/colorPrimary"
android:textSize="20sp"
android:textStyle="bold"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:orientation="horizontal">
<android.support.v7.widget.AppCompatButton
android:id="@+id/reset"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:elevation="5dp"
android:gravity="center"
android:text="Reset"
android:textAllCaps="true"
android:textColor="@color/white"
android:textStyle="bold" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/history"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:elevation="5dp"
android:gravity="center"
android:text="History"
android:textAllCaps="true"
android:textColor="@color/white"
android:textStyle="bold" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/pulseRequest"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:elevation="5dp"
android:gravity="center"
android:text="Request pulse"
android:textAllCaps="true"
android:textColor="@color/white"
android:textStyle="bold" />
</LinearLayout>
</RelativeLayout>
为了显示心率,我们有一个带有Imageview和Textview的LinearLayour,其中imageview将保持静态。相反,用我们在 Wear 模块中创建的HeartBeatView替换imageview,以实现心形自定义动画。让我们最后一次创建它。
在res/values文件夹内,添加heartbeatview_attrs.xml文件,并添加以下代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="HeartBeatView">
<attr name="scaleFactor" format="float" />
<attr name="duration" format="integer" />
</declare-styleable>
</resources>
在 drawables 中,创建一个矢量图形 XML 文件,并在其中添加以下代码以实现心形:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFF0000"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5
2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81
14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86
-8.55,11.54L12,21.35z"/>
</vector>
我们可以在 utils 包内创建一个名为HeartBearView的类,并添加以下代码以处理所有动画和自定义视图逻辑。关于实现的更多细节,可以参考前一章中的 Wear 模块HeartBeatView类:
public class HeartBeatView extends AppCompatImageView {
private static final String TAG = "HeartBeatView";
private static final float DEFAULT_SCALE_FACTOR = 0.2f;
private static final int DEFAULT_DURATION = 50;
private Drawable heartDrawable;
private boolean heartBeating = false;
float scaleFactor = DEFAULT_SCALE_FACTOR;
float reductionScaleFactor = -scaleFactor;
int duration = DEFAULT_DURATION;
public HeartBeatView(Context context) {
super(context);
init();
}
public HeartBeatView(Context context, AttributeSet attrs) {
super(context, attrs);
populateFromAttributes(context, attrs);
init();
}
public HeartBeatView(Context context, AttributeSet attrs, int
defStyleAttr) {
super(context, attrs, defStyleAttr);
populateFromAttributes(context, attrs);
init();
}
private void init() {
//make this not mandatory
heartDrawable = ContextCompat.getDrawable(getContext(),
R.drawable.ic_heart_red_24dp);
setImageDrawable(heartDrawable);
}
private void populateFromAttributes(Context context, AttributeSet
attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.HeartBeatView,
0, 0
);
try {
scaleFactor = a.getFloat(R.styleable
.HeartBeatView_scaleFactor, DEFAULT_SCALE_FACTOR);
reductionScaleFactor = -scaleFactor;
duration = a.getInteger(R.styleable.HeartBeatView_duration,
DEFAULT_DURATION);
} finally {
a.recycle();
}
}
/**
* toggles current heat beat state
*/
public void toggle() {
if (heartBeating) {
stop();
} else {
start();
}
}
/**
* Starts the heat beat/pump animation
*/
public void start() {
heartBeating = true;
animate().scaleXBy(scaleFactor)
.scaleYBy(scaleFactor).setDuration(duration)
.setListener(scaleUpListener);
}
/**
* Stops the heat beat/pump animation
*/
public void stop() {
heartBeating = false;
clearAnimation();
}
/**
* is the heart currently beating
*
* @return
*/
public boolean isHeartBeating() {
return heartBeating;
}
public int getDuration() {
return duration;
}
private static final int milliInMinute = 60000;
/**
* set the duration of the beat based on the beats per minute
*
* @param bpm (positive int above 0)
*/
public void setDurationBasedOnBPM(int bpm) {
if (bpm > 0) {
duration = Math.round((milliInMinute / bpm) / 3f);
}
}
public void setDuration(int duration) {
this.duration = duration;
}
public float getScaleFactor() {
return scaleFactor;
}
public void setScaleFactor(float scaleFactor) {
this.scaleFactor = scaleFactor;
reductionScaleFactor = -scaleFactor;
}
private final Animator.AnimatorListener scaleUpListener = new
Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//we ignore heartBeating as we want to ensure the heart is
reduced back to original size
animate().scaleXBy(reductionScaleFactor)
.scaleYBy(reductionScaleFactor).setDuration(duration)
.setListener(scaleDownListener);
}
@Override
public void onAnimationCancel(Animator animation) {
}
};
private final Animator.AnimatorListener scaleDownListener = new
Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (heartBeating) {
//duration twice as long for the upscale
animate().scaleXBy(scaleFactor).scaleYBy(scaleFactor)
.setDuration
(duration * 2).setListener(scaleUpListener);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
};
}
在activity_main.xml文件中,用项目中创建的自定义视图替换ImageView代码:
<com.packt.upbeat.utils.HeartBeatView
android:id="@+id/heartbeat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/heart"
android:layout_below="@+id/heart"/>
既然我们的着陆页用户界面已经准备好了,我们可以开始处理MainActivity。
在MainActivity中,让我们实例化我们在布局中使用的所有 UI 组件:
private AppCompatButton mReset, mHistory, mHeartPulse;
private TextView mSteps, mHeart, mCalory;
private HeartBeatView heartbeat;
在oncreate方法中使用findviewbyid方法将组件与其 ID 进行映射:
heartbeat = (HeartBeatView)findViewById(R.id.heartbeat);
mSteps = (TextView) findViewById(R.id.steps);
mHeart = (TextView) findViewById(R.id.heart);
mCalory = (TextView) findViewById(R.id.calory);
mReset = (AppCompatButton) findViewById(R.id.reset);
mHistory = (AppCompatButton) findViewById(R.id.history);
mHeartPulse = (AppCompatButton) findViewById(R.id.pulseRequest);
在HeartRateReceiver类中,获取数据,将数据转换为整数,并在 UI 中显示。以下代码说明如何使用从 Wear 应用接收的数据激活HeartBeatAnimation:
@Override
public void onReceive(Context context, Intent intent) {
String data = intent.getStringExtra("heart");
Log.v("heart", "Main activity received message: " + data);
mHeart.setText(data);
heartbeat.setDurationBasedOnBPM(Integer.valueOf(data));
heartbeat.toggle();
}
在StepReceiver中,我们将数据设置为标记为mSteps的步数textview:
mSteps.setText("Steps:"+ message);
我们已经完成了接收脉搏数和步数并在手机的 UI 中显示。现在,我们需要显示这些步骤消耗的卡路里。
根据您的身体质量指数等,可以通过多种不同方法从步数计算消耗的卡路里。关于计步器步数到卡路里的研究引入了一个转换因子,如下所示:
转换因子 = 每英里 99.75 卡路里 / 每英里 2,200 步 = 每步 0.045 卡路里
因此,使用这个值,我们可以通过简单地将这个值与步数相乘来确定卡路里。
消耗的卡路里 = 7,000 步 x 每步 0.045 卡路里 = 318 卡路里
在StepReceiver类中,在onReceive方法内,添加以下代码:
int value = Integer.valueOf(message);
mCalory.setText(String.valueOf((int)(value * 0.045)) + "/ncalories" + "/nburnt");
通过手机完成的卡路里消耗和脉搏率检查。在MainActivity中我们还有更多工作要做。我们需要将步数计数器的数据持久化,以显示卡路里和距离的历史记录。让我们使用第一章尝试过的RealmDB。
将以下 classpath 添加到项目级别的 gradle 文件中:
classpath "io.realm:realm-gradle-plugin:2.2.1"
在 gradle 移动模块中应用上一个插件:
apply plugin: 'realm-android'
项目中的 Realm 已准备就绪。现在,我们需要为步数数据设置 setters 和 getters。将以下类添加到 models 包中:
public class StepCounts extends RealmObject {
private String ReceivedDateTime;
private String Data;
public String getReceivedDateTime() {
return ReceivedDateTime;
}
public void setReceivedDateTime(String receivedDateTime) {
ReceivedDateTime = receivedDateTime;
}
public String getData() {
return Data;
}
public void setData(String data) {
Data = data;
}
}
在MainActivity中,实例化 Realm 并在onCreate方法中初始化,如下所示:
private Realm realm;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
Realm.init(this);
realm = Realm.getDefaultInstance();
}
当接收到步数时,将数据添加到RealmDB中。在StepReceiver内部类的onReceive方法内添加以下代码:
realm.beginTransaction();
StepCounts Steps = realm.createObject(StepCounts.class);
Steps.setData(message);
String TimeStamp = DateFormat.getDateTimeInstance().format(System.currentTimeMillis());
Steps.setReceivedDateTime(TimeStamp);
realm.commitTransaction();
为了在 UI 中显示最后一个值,在onCreate方法中添加以下代码:
RealmResults<StepCounts> results = realm.where(StepCounts.class).findAll();
if(results.size() == 0){
mSteps.setText("Steps: ");
}else{
mSteps.setText("Steps: "+results.get(results.size()-1).getData());
int value = Integer.valueOf(results
.get(results.size()-1).getData());
mCalory.setText(String.valueOf((int)(value * 0.045))
+ "/ncalories" + "/nburnt");
}
对于按钮,现在将点击监听器附加到onCreate方法中:
mHistory.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
mHeartPulse.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
mReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
让我们创建另一个 Activity,并将其命名为HistoryActivity,它将显示接收到的数据列表。在activity_history.xml文件中,添加以下代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.packt.upbeat.HistoryActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp" />
</LinearLayout>
现在,我们需要为recyclerview中的每个项目创建row_layout,布局如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="9dp"
card_view:cardCornerRadius="3dp"
card_view:cardElevation="0.01dp">
<LinearLayout
android:layout_margin="10dp"
android:orientation="vertical"
android:id="@+id/top_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="10dp"
android:id="@+id/steps"
android:text="Steps"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_margin="10dp"
android:id="@+id/calories"
android:text="calory"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_margin="10dp"
android:id="@+id/distance"
android:text="distance"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_margin="10dp"
android:id="@+id/date"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="bottom"
android:background="#ff444444"
android:gravity="center"
android:text="Timestamp"
android:textColor="#fff"
android:textSize="20dp" />
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
请记住,在使用cardview和recyclerview之前,我们需要将依赖项添加到我们的 gradle 模块中:
compile 'com.android.support:cardview-v7:25.1.1'
compile 'com.android.support:recyclerview-v7:25.1.1'
Recyclerview 适配器
我们将不得不创建一个adapter类,它从 Realm 获取数据并适配到创建的row_layout:
public class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.ViewHolder> {
public List<StepCounts> steps;
public Context mContext;
public HistoryAdapter(List<StepCounts> steps, Context mContext) {
this.steps = steps;
this.mContext = mContext;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.row_item, viewGroup, false);
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
viewHolder.steps.setText(steps.get(i).getData()+" Steps");
viewHolder.date.setText(steps.get(i).getReceivedDateTime());
int value = Integer.valueOf(steps.get(i).getData());
DecimalFormat df = new DecimalFormat("#.00") ;
String kms = String.valueOf(df.format(value * 0.000762)) + "
kms" ;
viewHolder.calory.setText(String.valueOf((int)(value * 0.045))
+ " calories " + "burnt");
viewHolder.distance.setText("Distance: "+kms);
}
@Override
public int getItemCount() {
return steps.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView steps,calory,distance,date;
public ViewHolder(View itemView) {
super(itemView);
steps = (TextView) itemView.findViewById(R.id.steps);
calory = (TextView) itemView.findViewById(R.id.calories);
distance = (TextView) itemView.findViewById(R.id.distance);
date = (TextView) itemView.findViewById(R.id.date);
}
}
}
在适配器中,我们使用转换因子值显示消耗的卡路里。为了找到通用距离,我们还有另一个值,需要将步数乘以它,如适配器所示。
在HistoryActivity中,在类全局范围内,声明以下实例:
Realm realm;
RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
RecyclerView.Adapter mAdapter;
现在,在HistoryActivity类的onCreate方法中,添加以下代码:
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
Realm.init(this);
realm = Realm.getDefaultInstance();
RealmResults<StepCounts> results = realm.where(StepCounts.class).findAll();
// The number of Columns
mLayoutManager = new GridLayoutManager(this, 1);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new HistoryAdapter(results,HistoryActivity.this);
mRecyclerView.setAdapter(mAdapter);
完成的HistoryActivity类
完整的类代码如下所示:
public class HistoryActivity extends AppCompatActivity {
Realm realm;
RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
RecyclerView.Adapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_history);
// Calling the RecyclerView
mRecyclerView = (RecyclerView)
findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
Realm.init(this);
realm = Realm.getDefaultInstance();
RealmResults<StepCounts> results =
realm.where(StepCounts.class).findAll();
// The number of Columns
mLayoutManager = new GridLayoutManager(this, 1);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new HistoryAdapter(results,HistoryActivity.this);
mRecyclerView.setAdapter(mAdapter);
}
}
在MainActivity中,当点击mHistory按钮时启动historyActivity:
mHistory.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this,
HistoryActivity.class));
}
});
现在,是时候使用我们在 Wear 中使用的方法,将数据从移动设备发送到 Wear 设备了。
我们将创建一个扩展了Thread的类,并使用 Node 和 Message API,以下方式发送数据:
class SendToDataLayerThread extends Thread {
String path;
String message;
// Constructor to send a message to the data layer
SendToDataLayerThread(String p, String msg) {
path = p;
message = msg;
}
public void run() {
NodeApi.GetConnectedNodesResult nodes =
Wearable.NodeApi.getConnectedNodes(googleClient).await();
for (Node node : nodes.getNodes()) {
MessageApi.SendMessageResult result =
Wearable.MessageApi.sendMessage(googleClient,
node.getId(), path, message.getBytes()).await();
if (result.getStatus().isSuccess()) {
Log.v("myTag", "Message: {" + message + "} sent to: " +
node.getDisplayName());
} else {
// Log an error
Log.v("myTag", "ERROR: failed to send Message");
}
}
}
}
在mHeartPulse按钮点击监听器内部,按如下方式启动SendToDataLayerThread类:
mHeartPulse.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new SendToDataLayerThread("/heart", "Start upbeat for heart
rate").start();
}
});
现在,切换回 Wear 项目范围,并添加一个扩展了WearableListenerService的新类。当它从移动应用接收到消息时,触发一个通知来启动应用程序。完整的类代码如下:
public class MobileListener extends WearableListenerService {
@Override
public void onMessageReceived(MessageEvent messageEvent) {
if (messageEvent.getPath().equals("/heart")) {
final String message = new String(messageEvent.getData());
Log.v("myTag", "Message path received on watch is: " +
messageEvent.getPath());
Log.v("myTag", "Message received on watch is: " + message);
// Broadcast message to wearable activity for display
Intent messageIntent = new Intent();
messageIntent.setAction(Intent.ACTION_SEND);
messageIntent.putExtra("message", message);
LocalBroadcastManager.getInstance(this)
.sendBroadcast(messageIntent);
Intent intent2 = new Intent
(getApplicationContext(), MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity
(getApplicationContext(), 0, intent2,
PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri
(RingtoneManager.TYPE_ALARM);
NotificationCompat.Builder notificationBuilder =
(NotificationCompat.Builder) new
NotificationCompat.Builder(getApplicationContext())
.setAutoCancel(true) //Automatically delete the
notification
.setSmallIcon(R.drawable.ic_heart_icon)
//Notification icon
.setContentIntent(pendingIntent)
.setContentTitle("Open upbeat")
.setContentText("UpBeat to check the pulse")
.setCategory(Notification.CATEGORY_REMINDER)
.setPriority(Notification.PRIORITY_HIGH)
.setSound(defaultSoundUri);
NotificationManagerCompat notificationManager =
NotificationManagerCompat.from
(getApplicationContext());
notificationManager.notify(0, notificationBuilder.build());
}
else {
super.onMessageReceived(messageEvent);
}
}
}
现在,在清单文件中使用正确的路径注册之前提到的服务,使用以下代码为移动应用注册:
<service android:name=".services.MobileListener">
<intent-filter>
<action android:name=
"com.google.android.gms.wearable.DATA_CHANGED" />
<action android:name=
"com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/heart"
android:scheme="wear" />
</intent-filter>
</service>
让我们切换回移动项目范围,完成重置按钮点击事件。我们将编写一个方法,该方法刷新RealmDB数据并重新创建活动:
public void Reset(){
RealmResults<StepCounts> results =
realm.where(StepCounts.class).findAll();
realm.beginTransaction();
results.deleteAllFromRealm();
realm.commitTransaction();
}
在点击监听器内部,以下方式添加以下方法:
mReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Reset();
recreate();
}
});
切换到 Wear 项目范围,并为健康建议创建一个新的 Activity,我们将这个活动称为HealthTipsActivity。在这个屏幕上,我们将列出一些好的健康建议和提示。
在activity_health_tips.xml中,添加以下代码:
<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
app:layout_box="all"
tools:deviceIds="wear">
<android.support.wearable.view.WearableRecyclerView
android:id="@+id/wearable_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.wearable.view.BoxInsetLayout>
我们需要为建议活动添加一个更多布局的行项目。我们将这个布局称为health_tips_row.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:tag="cards main container">
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardBackgroundColor="@color/colorPrimary"
card_view:cardCornerRadius="10dp"
card_view:cardElevation="5dp"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/health_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:text="HealthTip"
android:textColor="@color/white"
android:textAppearance="?
android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/tip_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:text="Details"
android:textColor="@color/white"
android:textAppearance="?
android:attr/textAppearanceMedium" />
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
创建一个包含所需字段的模型。我们将为所有字段创建带有完整参数化构造函数的设置器和获取器:
public class HealthTipsItem {
public String Title;
public String MoreInfo;
public HealthTipsItem(String title, String moreInfo) {
Title = title;
MoreInfo = moreInfo;
}
public String getTitle() {
return Title;
}
public void setTitle(String title) {
Title = title;
}
public String getMoreInfo() {
return MoreInfo;
}
public void setMoreInfo(String moreInfo) {
MoreInfo = moreInfo;
}
}
我们将有一个保存所有健康建议的另一个数据类:
public class HealthTips {
public static String[] nameArray =
{"Food style",
"Food style",
"Food style",
"Drinking water",
"Unhealthy drinks",
"Alcohol and drugs",
"Body Mass index",
"Physical excercise",
"Physical activities",
"Meditation",
"Healthy signs"};
public static String[] versionArray = {
"Along with fresh vegetables and fruits, eat lean meats (if
you're not vegetarian), nuts, and seeds.",
"Opt for seasonal and local products instead of those
exotic imported foodstuff",
"Make sure you get a proper balanced diet, as often as
possible",
"Drink water - you need to stay hydrated. It is great for
your internal organs, and it also keeps your skin healthy
and diminishes acne",
"Stop drinking too much caffeine and caffeinated
beverages",
"Limit alcohol intake. Tobacco and drugs should be a firm
No",
"Maintain a healthy weight.",
"Exercise at least four days a week for 20 to 30 minutes
each day. Another option is to break your workouts into
several sessions",
"Try to have as much physical activity as you can. Take the
stairs instead of elevator; walk to the market instead of
taking your car etc",
"Practice simple meditation. It balances your body, mind,
and soul",
"When speaking about health tips, skin, teeth, hair, and
nails are all health signs. Loss of hair or fragile nails
might mean poor nutrition"};
}
现在,我们将创建一个适配器来处理健康建议列表。以下代码获取数据并在wearablerecyclerview中加载:
public class RecyclerViewAdapter
extends WearableRecyclerView.Adapter
<RecyclerViewAdapter.ViewHolder> {
private List<HealthTipsItem> mListTips = new ArrayList<>();
private Context mContext;
public RecyclerViewAdapter(List<HealthTipsItem> mListTips, Context
mContext) {
this.mListTips = mListTips;
this.mContext = mContext;
}
static class ViewHolder extends RecyclerView.ViewHolder {
private TextView Title, info;
ViewHolder(View view) {
super(view);
Title = (TextView) view.findViewById(R.id.health_tip);
info = (TextView) view.findViewById(R.id.tip_details);
}
}
@Override
public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup
parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.health_tips_row, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.Title.setText(mListTips.get(position).getTitle());
holder.info.setText(mListTips.get(position).getMoreInfo());
}
@Override
public int getItemCount() {
return mListTips.size();
}
}
在活动的全局范围内,声明以下实例:
private RecyclerViewAdapter mAdapter;
private List<HealthTipsItem> myDataSet = new ArrayList<>();
在oncreate方法内部,我们可以通过添加以下代码来完成应用:
WearableRecyclerView recyclerView = (WearableRecyclerView) findViewById(R.id.wearable_recycler_view);
recyclerView.setHasFixedSize(true);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);
myDataSet = new ArrayList<HealthTipsItem>();
for (int i = 0; i < HealthTips.nameArray.length; i++) {
myDataSet.add(new HealthTipsItem(
HealthTips.nameArray[i],
HealthTips.versionArray[i]
));
}
mAdapter = new RecyclerViewAdapter(myDataSet,HealthTipsActivity.this);
recyclerView.setAdapter(mAdapter);
让我们创建另一个 Activity,用于通用卡路里图表,从国际食物列表中调用活动CalorychartActivity。
在CaloryChartActivity布局文件中,我们将添加WearableRecyclerView组件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
app:layout_box="all"
tools:deviceIds="wear">
<android.support.wearable.view.WearableRecyclerView
android:id="@+id/wearable_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.wearable.view.BoxInsetLayout>
为每个卡路里图表项目创建另一个布局,并添加以下代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:tag="cards main container">
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardBackgroundColor="@color/colorPrimary"
card_view:cardCornerRadius="10dp"
card_view:cardElevation="5dp"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/health_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:text="calory"
android:textColor="@color/white"
android:textAppearance="?
android:attr/textAppearanceLarge" />
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
我们将以下面的方式为卡路里创建model类:
public class CaloryItem {
public String Calories;
public CaloryItem(String calories) {
Calories = calories;
}
public String getCalories() {
return Calories;
}
public void setCalories(String calories) {
Calories = calories;
}
}
我们将创建另一个卡路里图表的适配器。该适配器与HealthTips适配器类似。创建一个文件RecyclerViewCaloryAdapter并将以下代码添加到其中:
public class RecyclerViewCaloryAdapter
extends WearableRecyclerView.Adapter<RecyclerViewCaloryAdapter.ViewHolder> {
private List<CaloryItem> mCalory = new ArrayList<>();
private Context mContext;
public RecyclerViewCaloryAdapter(List<CaloryItem> mCalory, Context
mContext) {
this.mCalory = mCalory;
this.mContext = mContext;
}
static class ViewHolder extends RecyclerView.ViewHolder {
private TextView Title;
ViewHolder(View view) {
super(view);
Title = (TextView) view.findViewById(R.id.health_tip);
}
}
@Override
public RecyclerViewCaloryAdapter.ViewHolder
onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.calory_row, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.Title.setText(mCalory.get(position).getCalories());
}
@Override
public int getItemCount() {
return mCalory.size();
}
}
在CaloryChartActivity项目的全局范围内,添加以下实例:
private RecyclerViewCaloryAdapter mAdapter;
private List<CaloryItem> myDataSet = new ArrayList<>();
在oncreate方法内部添加以下代码:
WearableRecyclerView recyclerView = (WearableRecyclerView) findViewById(R.id.wearable_recycler_view);
recyclerView.setHasFixedSize(true);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);
myDataSet = new ArrayList<CaloryItem>();
for (int i = 0; i < Calory.nameArray.length; i++) {
myDataSet.add(new CaloryItem(
Calory.nameArray[i]
));
}
mAdapter = new RecyclerViewCaloryAdapter(myDataSet,CaloryChartActivity.this);
recyclerView.setAdapter(mAdapter);
以下屏幕显示了完整的移动和 Wear 应用。
下图展示了监听步骤和脉搏的活动屏幕:
下图展示了 Wear 应用中健康食物建议屏幕。它使用WearableRecyclerView进行设计:
以下图像展示了用于学习不同食品项中可提供热量的热量图表:
总结
在本章中,我们了解了与穿戴设备和移动应用程序协作的基础知识。我们已经探索了从穿戴设备发送和接收数据到移动设备以及反向支持的 API。现在,为任何穿戴项目集成RealmDB将会更加容易。
在下一章中,我们将为穿戴设备构建一个谷歌地图应用程序,并且我们将持久化位置数据,了解穿戴设备的不同地图类型和控制方法。
第六章:任意地点的出行方式 - WearMap 和 Google API 客户端
地图是区域或区域部分的视觉表示。
我们人类会去不同的城市旅行,这些城市可能是国内的也可能是国际的。那么,追踪你所访问的地方怎么样?我们出于不同的原因使用地图,但在大多数情况下,我们使用地图来规划特定的活动,比如户外游览、骑自行车和其他类似活动。地图帮助人类智能找到从起点到目的地的最快路线。在这个项目中,我们将构建一个与 Google Maps 服务配合工作的 Wear 应用程序。
记录一下,Google 地图最初是在 2004 年 10 月作为一个 C++ 桌面程序开始的。Google 地图在 2005 年 2 月正式发布。Google 地图提供了一个 API,允许将地图嵌入第三方应用程序中;Google 地图提供了许多地方的空中和卫星视图。与其他地图服务相比,Google 地图是最佳的,地图进行了优化且其准确率非常高。
在这个项目中,让我们构建一个独立的 Wear 地图应用程序。当用户点击地图时,我们将允许用户写下关于该地点的故事并将其保存到 SQLite 数据库中作为一个标记。当用户点击标记时,我们应该向用户展示已保存的内容。在本章中,我们将了解以下重要概念:
-
在开发者 API 控制台中创建项目
-
使用 SHA1 指纹获取 Maps API 密钥
-
SQLite 集成
-
Google 地图
-
Google API 客户端及更多功能
-
地理编码器
让我们开始创建 WearMap
现在我们知道如何创建一个独立的应用程序。如果你是直接跟随这个项目,而没有参考第 第二章,让我们帮助你捕捉心中所想 - WearRecyclerView 和更多 和第 第三章,让我们帮助你捕捉心中所想 - 保存数据和定制 UI 中介绍的 Wear-note 应用程序,请务必跟进 Wear-note 应用程序以了解更多关于独立应用程序的信息。
我们将这个项目称为 WearMapDiary,因为我们存储的是地点及其详细信息。项目的包地址由开发者决定;在这个项目中,包地址是 com.packt.wearmapdiary,API 级别为 25 Nougat。在活动模板中,选择 Google Maps Wear 活动,如下所示:
从活动选择器中选择 Google Maps Wear 活动模板
创建项目后,我们将看到项目的必要配置,其中包括已经添加的地图片段;它将设置 DismissOverlays 组件:
将为与 Wear 地图活动一起工作而配置的示例代码生成。
我们需要在 res/values 目录下的 google_maps_api.xml 文件中为项目添加 Maps API 密钥:
Google API 控制台
Google API 控制台是一个网络门户,允许开发者为他们的项目开发管理 Google 服务,可以通过console.developers.google.com访问。
- 使用你的 Google 账户访问开发者控制台。创建一个项目
packt-wear或对开发者来说方便的其他名称:
- 成功创建项目后,前往 API 管理器 | 库部分,并启用 Google Maps Android API:
- 点击“启用”按钮,为 Android 启用地图:
- 在控制台启用 API 后,我们需要使用开发机器的 SHA1 指纹和项目的包地址创建 API 密钥,如下所示:
-
要获取你的设备的 SHA1 指纹,请打开 Android Studio。在 Android Studio 的右侧,你会看到 Gradle 项目菜单。然后,按照以下步骤操作:
-
点击“Gradle”(在右侧面板上,你会看到 Gradle 栏)
-
点击“刷新”(在 Gradle 栏中点击“刷新”;你将看到项目的 Gradle 脚本列表)
-
点击你的项目(从列表(根)中的项目名称)
-
点击“任务”
-
点击“Android”
-
-
- 双击 signingReport(你将在运行栏中获得 SHA1 和 MD5):
-
- 复制你的 SHA1 指纹,粘贴到 Google API 控制台,并保存:
-
- 现在,从控制台复制 API 密钥,并将其粘贴到项目的
google_maps_api.xml文件中,如下所示:
- 现在,从控制台复制 API 密钥,并将其粘贴到项目的
-
- 现在,将你的 Gradle 范围切换到应用,并在 Wear 模拟器或你的 Wear 设备中编译项目:
如果你的模拟器中没有更新 Google Play 服务,Wear 会显示一个错误屏幕以更新 Play 服务:
如果你有一个实际的 Wear 设备,当最新的 Google Play 服务更新可用时,Wear 操作系统将负责下载更新。对于模拟器,我们需要将其连接到实际设备以添加账户。首先,通过 adb 连接 Android 手机并启动 Wear 模拟器。
从 Play 商店安装 Android Wear 伴侣应用play.google.com/store/apps/details?id=com.google.android.wearable.app&hl=en。
在 Android Wear 应用程序中,选择模拟器,然后在 Android Studio 终端中输入以下命令:
adb -d forward tcp:5601 tcp:5601
当模拟器连接到你的真实手机后,你可以添加已经同步到手机的账户,或者添加一个新的账户。
下图展示了 Wear 账户的同步屏幕:
- 成功添加账户后,开始更新你的 Google Play 服务:
- 现在,完成所有这些配置后,在 Android Studio 中编译程序,并在 Wear 设备上查看地图:
Google API 客户端
GoogleApiClient扩展了Object类。Google API 客户端为所有 Google Play 服务提供了一个共同的入口点,并在用户设备与每个 Google 服务之间管理网络连接。Google 建议使用GoogleApiClient以编程方式获取用户的位置。
在每个线程上创建GoogleApiClient。GoogleApiClient服务连接在内部被缓存。GoogleApiClient实例不是线程安全的,因此创建多个实例很快。GoogleApiClient与各种静态方法一起使用。其中一些方法要求GoogleApiClient已连接;有些会在GoogleApiClient连接之前排队调用。
下面是一个与 Google LocationServices 连接创建GoogleApiClient实例的代码示例:
GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
配置项目以实现功能
我们知道为了更好的代码管理和将来维护代码,创建包的重要性。让我们为项目创建一个包含四个不同名称的包,分别是 adapter、model、util 和 view。
我们在 model 包内编写我们的普通旧 Java 对象。我们将在 util 包中配置所有与数据库相关的类,以及在 view 包中配置自定义视图,如对话框片段、TextView等。对于自定义infoWindow,我们必须在adapter包内创建一个infoWindowAdapter。
使用GoogleApiClient获取位置信息非常重要。现在我们已经配置了 Wear 地图活动,并使用我们添加的 API 密钥绘制了地图,是时候利用GoogleApiClient获取位置详情了。
利用GoogleApiClient获取用户的位置信息
现在,在MapActivity类中,我们需要实现以下接口:
-
GoogleApiClient.ConnectionCallback -
GoogleApiClient.OnConnectionFailedListener
然后,我们需要从这两个接口重写三个方法,它们分别是:
-
public void onConnected(..){} -
public void onConnectionSuspended(..){} -
public void onConnectionFailed(..){}
在onConnected方法中,我们可以使用GoogleApiClient实例实例化位置服务。首先,让我们将GoogleApiClient添加到项目中。在MapActivity的全局范围内创建一个GoogleApiClient实例:
private GoogleApiClient mGoogleApiClient;
添加一个名为addGoogleAPIClient(){ }的 void 方法,用于获取位置服务 API:
private void addGoogleAPIClient(){
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
}
为了让 Google Play 服务处理与位置相关的任务,请在 gradle wear 模块中添加以下依赖项:
compile 'com.google.android.gms:play-services-location:11.0.2'
现在,在onConnected方法中,附加mGoogleApiClient:
@Override
public void onConnected(@Nullable Bundle bundle) {
Location location = LocationServices.FusedLocationApi
.getLastLocation(mGoogleApiClient);
double latitude = location.getLatitude();
double longitude = location.getLongitude();
}
Locationservice在请求位置之前需要权限检查。让我们在 manifest 和 Activity 中添加权限。
在 Manifest 中添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- The following two permissions are not required to use
Google Maps Android API v2, but are recommended. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
在MapActivity.java类中编写一个检查权限的方法,如下所示:
private boolean checkPermission(){
int result = ContextCompat.checkSelfPermission(MapsActivity.this,
Manifest.permission.ACCESS_FINE_LOCATION);
if (result == PackageManager.PERMISSION_GRANTED){
return true;
} else {
return false;
}
}
按如下方式重写onRequestPermissionsResult(..){}方法:
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSION_REQUEST_CODE:
if (grantResults.length > 0 && grantResults[0] ==
PackageManager.PERMISSION_GRANTED) {
} else {
}
break;
}
}
现在,我们有了权限检查方法;在onConnected方法中处理它:
@Override
public void onConnected(@Nullable Bundle bundle) {
if (checkPermission()) {
Location location = LocationServices.FusedLocationApi
.getLastLocation(mGoogleApiClient);
double latitude = location.getLatitude();
double longitude = location.getLongitude();
}else{
}
}
让我们编写一个方法来检查 Wear 设备上是否内置了 GPS。通过使用packagemanager类,我们可以检索 Wear 设备上可用的硬件。让我们写一个名为hasGps()的方法:
private boolean hasGps() {
return getPackageManager().hasSystemFeature(
PackageManager.FEATURE_LOCATION_GPS);
}
如果你想要用户知道他们的设备是否有 GPS 设备,或者在开发过程中只是想要记录下来,你可以在onCreate()方法中使用这个方法:
if (!hasGps()) {
Log.d(TAG, "This hardware doesn't have GPS.");
// Fall back to functionality that does not use location or
// warn the user that location function is not available.
}
如果你的可穿戴应用使用内置 GPS 记录数据,你可能想要通过实现onLocationChanged()方法,使用LocationListner接口将位置数据与手持设备同步。
要使你的应用能够感知位置,请使用GoogleAPIclient。
想要了解更多关于权限的信息,请点击这个链接:developer.android.com/training/articles/wear-permissions.html。
现在,让我们处理onMapclick方法,以处理在地图上添加标记的过程。为此,在你的活动中实现GoogleMap.OnMapClickListener并实现其回调方法,这将为你提供带有经纬度的onmapclick。将点击上下文添加到你的onMapReady回调中,如下所示:
mMap.setOnMapClickListener(this);
在onMapClick方法中,我们可以使用latLng添加以下标记:
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "Latlng is "+latLng);
}
在onMapclick方法中添加标记使用MarkerOptions()。对于谷歌设计的高级标记,我们将使用地图的addmarker方法,并添加带有位置、标题和摘要(标题下方的简短描述)的新MarkerOptions:
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "Latlng is "+latLng);
mMap.addMarker(new MarkerOptions()
.position(latLng)
.title("Packt wear 2.0")
.snippet("Map is cool in wear device"));
}
添加带有infowindow的标记后:
现在,我们已经有了地图,并且正在向地图添加标记,但我们需要处理地理编码以获取坐标的地址名称。
使用 GeoCoder 的地理空间数据
使用GeoCoder类通过坐标获取地址。地理编码通常是将街道地址或位置的其它描述转换为(纬度,经度)坐标的过程。逆地理编码是将(纬度,经度)坐标转换为(部分)地址的过程。
在OnMapClick方法中,进行以下更改:
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "Latlng is "+latLng);
//Fetching the best address match
Geocoder geocoder = new Geocoder(this);
List<Address> matches = null;
try {
matches = geocoder.getFromLocation(latLng.latitude,
latLng.longitude, 1);
} catch (IOException e) {
e.printStackTrace();
}
Address bestAddress = (matches.isEmpty()) ? null : matches.get(0);
int maxLine = bestAddress.getMaxAddressLineIndex();
mMap.addMarker(new MarkerOptions()
.position(latLng)
.title(bestAddress.getAddressLine(maxLine - 1))
.snippet(bestAddress.getAddressLine(maxLine)));
}
上述代码片段将标记添加到地图上,并在信息窗口中显示位置名称:
在点击地图时弹出的视图在 Android 中称为infowindow。它类似于网页开发中的 ToolTip 组件。在这个项目中,我们需要在用户点击地图的任何地方保存数据;我们需要借助infowindow显示自定义地图标记。我们需要编写一个适配器,实现GoogleMap.InfoWindowAdapter与自定义布局,如下所示:
信息窗口适配器
以下实现解释了如何为地图标记编写我们自定义的infowindow适配器:
//XML latout for customising infowindow
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/snippet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
windowadapter类实现了GoogleMap.InfoWindowAdapter,包含两个回调方法getInfoWindow(..){}和getInfoContents(..){}。我们可以通过getInfoContent方法来填充自定义布局:
public class WearInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {
private LayoutInflater mLayoutInflater;
private View mView;
MarkerAdapter(LayoutInflater layoutInflater){
mLayoutInflater = layoutInflater;
}
@Override
public View getInfoWindow(Marker marker) {
return null;
}
@Override
public View getInfoContents(Marker marker) {
if (mView == null){
mView = mLayoutInflater.inflate(R.layout.marker, null);
}
TextView titleView = (TextView)mView.findViewById(R.id.title);
titleView.setText(marker.getTitle());
TextView snippetView =
(TextView)mView.findViewById(R.id.snippet);
snippetView.setText(marker.getSnippet());
return mView;
}
}
为了更好的代码访问和维护,将前面的适配器类添加到 adapter 包中。InfoWindowAdapter没有使用任何数据来填充视图;我们使用与标记关联的任何数据来填充视图。如果我们想在标题和摘要之外添加任何内容,适配器本身无法做到这一点。我们需要创建一个机制以编程方式实现这一点。
在 model 包中创建Memory类。Memory类是用户选择添加标记的地方:
public class Memory {
double latitude;
double longitude;
String city; // City name
String country; // Country name
String notes; // saving notes on the location
}
现在,我们已经有了 memory,自定义infowindow适配器已准备好与onMapclick实现一起工作。对于每个标记,我们将添加一个 memory 类关联。为了临时保存所有 memory,让我们使用HashMap:
private HashMap<String, Memory> mMemories = new HashMap<>();
让我们将标记添加到HashMap中,以便访问Marker属性,例如Marker ID 等。适配器的完整代码如下:
public class WearInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {
public LayoutInflater mLayoutInflater;
public View mView;
public HashMap<String, Memory> mMemories;
WearInfoWindowAdapter(LayoutInflater layoutInflater,
HashMap<String,Memory> memories){
mLayoutInflater = layoutInflater;
mMemories = memories;
}
@Override
public View getInfoWindow(Marker marker) {
return null;
}
@Override
public View getInfoContents(Marker marker) {
if (mView == null) {
mView = mLayoutInflater.inflate(R.layout.marker, null);
}
Memory memory = mMemories.get(marker.getId());
TextView titleView = (TextView)mView.findViewById(R.id.title);
titleView.setText(memory.city);
TextView snippetView =
(TextView)mView.findViewById(R.id.snippet);
snippetView.setText(memory.country);
TextView notesView = (TextView)mView.findViewById(R.id.notes);
notesView.setText(memory.notes);
return mView;
}
}
在OnMapClick方法中,添加以下更改:
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "Latlng is "+latLng);
Geocoder geocoder = new Geocoder(this);
List<Address> matches = null;
try {
matches = geocoder.getFromLocation(latLng.latitude,
latLng.longitude, 1);
} catch (IOException e) {
e.printStackTrace();
}
Address bestAddress = (matches.isEmpty()) ? null : matches.get(0);
int maxLine = bestAddress.getMaxAddressLineIndex();
Memory memory = new Memory();
memory.city = bestAddress.getAddressLine(maxLine - 1);
memory.country = bestAddress.getAddressLine(maxLine);
memory.latitude = latLng.latitude;
memory.longitude = latLng.longitude;
memory.notes = "Packt and wear 2.0 notes...";
Marker marker = mMap.addMarker(new MarkerOptions()
.position(latLng));
mMemories.put(marker.getId(), memory);
}
使用以下代码在onMapready方法中将新的Marker附加到地图上:
mMap.setInfoWindowAdapter(new WearInfoWindowAdapter(getLayoutInflater(), mMemories));
现在,编译程序。你应该能够看到如下更新的infoWindow:
用于记录位置信息的自定义DialogFragment
DialogFragment是一个在活动中浮动的对话框窗口。在 Wear 设备上它不会浮动,但它提供了 Wear 优化的设计。查看以下实现代码。
在继续之前,将 Memory 类实现为可序列化接口:
public class Memory implements Serializable {
public double latitude;
public double longitude;
public String city;
public String country;
public String notes;
}
在 layout 目录中添加以下布局文件,并将布局文件命名为memory_dialog_fragment.xml。创建文件后,在布局文件内添加以下代码:
<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MapsActivity"
tools:deviceIds="wear">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp"
app:layout_box="all"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:id="@+id/city"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/country"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/notes"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</android.support.wearable.view.BoxInsetLayout>
创建布局文件后,让我们处理创建自定义对话框的 Java 代码。创建一个名为MemoryDialogFragment的类,并继承自DialogFragment。
创建一个接口来处理DialogFragment的SaveClicked和cancelClicked按钮:
public interface Listener{
public void OnSaveClicked(Memory memory);
public void OnCancelClicked(Memory memory);
}
现在,将以下实例添加到MemoryDialogFragment的全局范围内。:
private static final String TAG = "MemoryDialogFragment";
private static final String MEMORY_KEY = "MEMORY";
private Memory mMemory;
private Listener mListener;
private View mView;
现在,让我们处理正确地在正确字段中填充布局的数据:
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mView = getActivity().getLayoutInflater()
.inflate(R.layout.memory_dialog_fragment, null);
TextView cityView = (TextView) mView.findViewById(R.id.city);
cityView.setText(mMemory.city);
TextView countryView = (TextView) mView.findViewById(R.id.country);
countryView.setText(mMemory.country);
AlertDialog.Builder builder = new
AlertDialog.Builder(getActivity());
builder.setView(mView)
.setTitle(getString(R.string.dialog_title))
.setPositiveButton(getString(R.string.DialogSaveButton),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
EditText notesView = (EditText)
mView.findViewById(R.id.notes);
mMemory.notes = notesView.getText().toString();
mListener.OnSaveClicked(mMemory);
}
})
.setNegativeButton(getString(R.string.DialogCancelButton),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
mListener.OnCancelClicked(mMemory);
}
});
return builder.create();
}
我们将在oncreate方法中从Memory获取序列化的数据:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null){
mMemory = (Memory)args.getSerializable(MEMORY_KEY);
}
}
MemoryDialogFragment的完整代码如下:
public class MemoryDialogFragment extends DialogFragment {
private static final String TAG = "MemoryDialogFragment";
private static final String MEMORY_KEY = "MEMORY";
private Memory mMemory;
private Listener mListener;
private View mView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null){
mMemory = (Memory)args.getSerializable(MEMORY_KEY);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mView = getActivity().getLayoutInflater()
.inflate(R.layout.memory_dialog_fragment, null);
TextView cityView = (TextView) mView.findViewById(R.id.city);
cityView.setText(mMemory.city);
TextView countryView = (TextView)
mView.findViewById(R.id.country);
countryView.setText(mMemory.country);
AlertDialog.Builder builder = new
AlertDialog.Builder(getActivity());
builder.setView(mView)
.setTitle(getString(R.string.dialog_title))
.setPositiveButton(getString
(R.string.DialogSaveButton),
new DialogInterface.OnClickListener() {
@Override
public void onClick
(DialogInterface dialog, int which) {
EditText notesView = (EditText)
mView.findViewById(R.id.notes);
mMemory.notes = notesView.getText().toString();
mListener.OnSaveClicked(mMemory);
}
})
.setNegativeButton(getString
(R.string.DialogCancelButton),
new DialogInterface.OnClickListener() {
@Override
public void onClick
(DialogInterface dialog, int which) {
mListener.OnCancelClicked(mMemory);
}
});
return builder.create();
}
public static MemoryDialogFragment newInstance(Memory memory){
MemoryDialogFragment fragment = new MemoryDialogFragment();
Bundle args = new Bundle();
args.putSerializable(MEMORY_KEY, memory);
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try{
mListener = (Listener)getActivity();
}catch (ClassCastException e){
throw new IllegalStateException("Activity does not
implement contract");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
public interface Listener{
public void OnSaveClicked(Memory memory);
public void OnCancelClicked(Memory memory);
}
}
在OnMapClick方法中,进行以下更改:
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "Latlng is "+latLng);
Memory memory = new Memory();
updateMemoryPosition(memory, latLng);
MemoryDialogFragment.newInstance(memory)
.show(getFragmentManager(),MEMORY_DIALOG_TAG);
}
现在,编译程序。在mapclick时,你将看到以下屏幕。用户可以在 edittext 字段中输入关于地图位置的自己的想法:
现在我们已经添加了输入对话框,让我们来处理将数据保存到 SQLite 的操作。
配置 SQLite 并保存标记
对于任何优秀的软件来说,持久化所有必要的数据都是基本用例。Android SDK 内置了 SQLite 存储解决方案。它占用的空间非常小,速度也非常快。如果程序员熟悉 SQL 查询和操作,那么使用 SQLite 将会轻松愉快。
模式和合约
本质上,对于数据库,我们需要创建一个数据模式,这是对数据库组织方式的正式声明。该模式反映在 SQLite 查询语句中。合约类是一个包含常量的容器,这些常量定义了 URI、表和列的名称。合约类允许在同一个包中的所有其他类中使用相同的常量。
对于WearMapDiary的范围,我们将在DBHelper类中创建所有实例。现在,让我们创建DBhelper类,它打开并连接应用程序到 SQLite,并处理查询:
public class DbHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "traveltracker.db";
private static final int DATABASE_VERSION = 3;
public static final String MEMORIES_TABLE = "memories";
public static final String COLUMN_LATITUDE = "latitude";
public static final String COLUMN_LONGITUDE = "longitude";
public static final String COLUMN_CITY = "city";
public static final String COLUMN_COUNTRY = "country";
public static final String COLUMN_NOTES = "notes";
public static final String COLUMN_ID = "_id";
private static DbHelper singleton = null;
public static DbHelper getInstance(Context context){
if (singleton == null){
singleton = new DbHelper(context.getApplicationContext());
}
return singleton;
}
private DbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE "+MEMORIES_TABLE+" ("
+COLUMN_ID+" INTEGER PRIMARY KEY AUTOINCREMENT, "
+COLUMN_LATITUDE +" DOUBLE, "
+COLUMN_LONGITUDE +" DOUBLE, "
+COLUMN_CITY +" TEXT, "
+COLUMN_COUNTRY +" TEXT, "
+COLUMN_NOTES +" TEXT"
+")");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int
newVersion) {
db.execSQL("DROP TABLE IF EXISTS "+MEMORIES_TABLE);
onCreate(db);
}
}
我们需要创建一个Datasource来管理所有查询,以及读写 SQLite 中的数据。在这里,这个类中,我们将创建多个方法来创建数据、读取数据、更新数据和删除数据:
public class MemoriesDataSource {
private DbHelper mDbHelper;
private String[] allColumns = {
DbHelper.COLUMN_ID, DbHelper.COLUMN_CITY,
DbHelper.COLUMN_COUNTRY, DbHelper.COLUMN_LATITUDE,
DbHelper.COLUMN_LONGITUDE, DbHelper.COLUMN_NOTES
};
public MemoriesDataSource(Context context){
mDbHelper = DbHelper.getInstance(context);
}
public void createMemory(Memory memory){
ContentValues values = new ContentValues();
values.put(DbHelper.COLUMN_NOTES, memory.notes);
values.put(DbHelper.COLUMN_CITY, memory.city);
values.put(DbHelper.COLUMN_COUNTRY, memory.country);
values.put(DbHelper.COLUMN_LATITUDE, memory.latitude);
values.put(DbHelper.COLUMN_LONGITUDE, memory.longitude);
memory.id = mDbHelper.getWritableDatabase()
.insert(DbHelper.MEMORIES_TABLE, null, values);
}
public List<Memory> getAllMemories(){
Cursor cursor = allMemoriesCursor();
return cursorToMemories(cursor);
}
public Cursor allMemoriesCursor(){
return mDbHelper.getReadableDatabase()
.query(DbHelper.MEMORIES_TABLE,
allColumns,null, null, null, null, null);
}
public List<Memory> cursorToMemories(Cursor cursor){
List<Memory> memories = new ArrayList<>();
cursor.moveToFirst();
while (!cursor.isAfterLast()){
Memory memory = cursorToMemory(cursor);
memories.add(memory);
cursor.moveToNext();
}
return memories;
}
public void updateMemory(Memory memory){
ContentValues values = new ContentValues();
values.put(DbHelper.COLUMN_NOTES, memory.notes);
values.put(DbHelper.COLUMN_CITY, memory.city);
values.put(DbHelper.COLUMN_COUNTRY, memory.country);
values.put(DbHelper.COLUMN_LATITUDE, memory.latitude);
values.put(DbHelper.COLUMN_LONGITUDE, memory.longitude);
String [] whereArgs = {String.valueOf(memory.id)};
mDbHelper.getWritableDatabase().update(
mDbHelper.MEMORIES_TABLE,
values,
mDbHelper.COLUMN_ID+"=?",
whereArgs
);
}
public void deleteMemory(Memory memory){
String [] whereArgs = {String.valueOf(memory.id)};
mDbHelper.getWritableDatabase().delete(
mDbHelper.MEMORIES_TABLE,
mDbHelper.COLUMN_ID+"=?",
whereArgs
);
}
private Memory cursorToMemory(Cursor cursor){
Memory memory = new Memory();
memory.id = cursor.getLong(0);
memory.city = cursor.getString(1);
memory.country = cursor.getString(2);
memory.latitude = cursor.getDouble(3);
memory.longitude = cursor.getDouble(4);
memory.notes = cursor.getString(5);
return memory;
}
}
为了在后台使用cursorLoader执行所有这些查询,我们将编写另一个类,我们将这个类称为DBCurserLoader:
public abstract class DbCursorLoader extends AsyncTaskLoader<Cursor> {
private Cursor mCursor;
public DbCursorLoader(Context context){
super(context);
}
protected abstract Cursor loadCursor();
@Override
public Cursor loadInBackground() {
Cursor cursor = loadCursor();
if (cursor != null){
cursor.getCount();
}
return cursor;
}
@Override
public void deliverResult(Cursor data) {
Cursor oldCursor = mCursor;
mCursor = data;
if (isStarted()){
super.deliverResult(data);
}
if (oldCursor != null && oldCursor != data){
onReleaseResources(oldCursor);
}
}
@Override
protected void onStartLoading() {
if (mCursor != null){
deliverResult(mCursor);
}
if (takeContentChanged() || mCursor == null){
forceLoad();
}
}
@Override
protected void onStopLoading() {
cancelLoad();
}
@Override
public void onCanceled(Cursor data) {
super.onCanceled(data);
if (data != null) {
onReleaseResources(data);
}
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
if (mCursor != null){
onReleaseResources(mCursor);
}
mCursor = null;
}
private void onReleaseResources(Cursor cursor){
if (!cursor.isClosed()){
cursor.close();
}
}
}
创建另一个类,用于从memoryDatasource加载所有记忆,并扩展到DBCursorLoader:
public class MemoriesLoader extends DbCursorLoader {
private MemoriesDataSource mDataSource;
public MemoriesLoader(Context context, MemoriesDataSource
memoriesDataSource){
super(context);
mDataSource = memoriesDataSource;
}
@Override
protected Cursor loadCursor() {
return mDataSource.allMemoriesCursor();
}
}
现在,我们的 SQLite 配置工作正常。让我们在MapActivity中处理保存数据到 SQLite 的onMapclick。
在 SQLite 中保存数据
要将 SQLite 连接到活动,并在 SQLite 中保存数据,请实现活动LoaderManager.LoaderCallbacks<Cursor>并在onCreate方法中实例化数据源:
mDataSource = new MemoriesDataSource(this);
getLoaderManager().initLoader(0,null,this);
实现LoaderManager.LoaderCallbacks<Cursor>接口的回调方法:
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return null;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
现在,将addingMarker代码重构为一个方法,如下所示:
private void addMarker(Memory memory) {
Marker marker = mMap.addMarker(new MarkerOptions()
.draggable(true)
.position(new LatLng(memory.latitude, memory.longitude)));
mMemories.put(marker.getId(), memory);
}
我们仍然需要处理拖动标记以用于将来的实现。让我们将可拖动属性设置为 true。现在,在OnMapClick方法中,调用以下代码:
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "Latlng is "+latLng);
Memory memory = new Memory();
updateMemoryPosition(memory, latLng);
MemoryDialogFragment.newInstance(memory)
.show(getFragmentManager(),MEMORY_DIALOG_TAG);
}
让我们重构UpdateMemoryPosition方法,它从latlng获取地址并将其添加到Memory:
private void updateMemoryPosition(Memory memory, LatLng latLng) {
Geocoder geocoder = new Geocoder(this);
List<Address> matches = null;
try {
matches = geocoder.getFromLocation(latLng.latitude,
latLng.longitude, 1);
} catch (IOException e) {
e.printStackTrace();
}
Address bestMatch = (matches.isEmpty()) ? null : matches.get(0);
int maxLine = bestMatch.getMaxAddressLineIndex();
memory.city = bestMatch.getAddressLine(maxLine - 1);
memory.country = bestMatch.getAddressLine(maxLine);
memory.latitude = latLng.latitude;
memory.longitude = latLng.longitude;
}
现在,我们正在 SQLite 中保存数据。当我们关闭并重新打开地图时,我们没有读取并将标记数据添加到地图中:
现在,让我们读取 SQLite 数据并将其添加到地图中。
LoaderManager类的onCreateLoader回调方法通过Datasource实例将数据添加到MemoryLoader,如下所示:
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.d(TAG,"onCreateLoader");
return new MemoriesLoader(this, mDataSource);
}
在onLoadFinished方法中,从游标中获取数据并将其添加到地图中:
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
Log.d(TAG,"onLoadFinished");
onFetchedMemories(mDataSource.cursorToMemories(cursor));
}
从获取的数据中向地图添加标记:
private void onFetchedMemories(List<Memory> memories) {
for(Memory memory: memories){
addMarker(memory);
}
}
现在,我们有一个名为WearMapDiary的功能性 Wear 应用,它查找地址并在地图上关于位置保存快速笔记。它将标记添加到 SQLite 数据库中,并在我们在 Wear 设备上打开应用程序时将标记附加到地图上:
从应用中检索笔记和位置信息:
在此对话框中,用户可以输入他或她想要保存在当前位置的数据:
到目前为止,我们已经探讨了如何将地图集成到穿戴设备上,并清楚地了解了如何获取地图 API 密钥。我们使用GoogleApiclient来获取位置服务。我们正在检查 GPS 硬件的可用性:
以下步骤简要介绍了如何编写自定义标记。
-
通过实现
GoogleMap.InfoWindowAdapter探索了自定义InfoWindow适配器。 -
使用
boxinsetlayout为 Wear 兼容性创建了自定义的dialogFragment。 -
Geocoder类获取GeoSpatial数据 -
SQLite 及其与地图数据的集成
现在,是时候了解关于穿戴设备上的地图的更多信息了。
独立地图应用与移动同步地图应用之间的区别
面向 Wear 2.0 平台的手表应用可以通过板载 Wi-Fi 传输器连接到 Wi-Fi。我们可以缓存地图等更多内容,但它仍然缺乏移动地图应用程序的舒适性。通常,对于独立的 Wear 应用,目标 API 级别是 25,并带有安全操作运行时权限。在本章中,我们添加了处理运行时权限的代码。
将应用标识为独立应用
Wear 2.0 要求手表应用的 Android Manifest文件中有一个新的元数据元素,作为<application>元素的子元素。新元数据元素的名称是com.google.android.wearable.standalone,值必须是 true 或 false:
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />
由于独立应用是独立或半独立的,因此 iPhone 用户和缺少 Play 商店的 Android 手机(如 BlackBerry android 分叉操作系统和诺基亚定制 Android 手机)可以安装它们。
如果手表应用依赖于手机应用,请将前一个元数据元素的值设置为 false。
即使值是 false,手表应用也可以在相应的手机应用安装之前安装。因此,如果手表应用检测到配套手机缺少必要的手机应用,手表应用应提示用户安装手机应用。
在手表应用和手机应用之间共享数据
手表应用和手机应用之间可以共享数据,或者共享特定于应用的数据。你可以使用标准的 Android 存储 API 在本地存储数据。例如,你可以使用SharedPreferences APIs,SQLite,或者内部存储(就像在手机上一样)。消息传递 API 的手表应用可以与对应的手机应用通信。
从另一台设备上检测你的应用
在CapabilityAPI中,你的 Wear 应用可以检测到与 Wear 应用对应的手机应用。Wear 设备可以自发地以及静态地向配对的设备广播它们的事件。要检查配对 Wear 设备宣传的功能,更多信息请查看此链接:developer.android.com/training/wearables/data-layer/messages.html#AdvertiseCapabilities。
请注意,并非所有手机都支持 Play 商店(如 iPhone 等)。本节描述了这些情况的最佳实践:你的独立手表应用需要你的手机应用,而你的手机应用也需要你的独立手表应用。
指定功能名称以检测你的应用
对于每种设备类型(手表或手机)对应的应用,请在res/values/wear.xml文件中为功能名称指定一个唯一的字符串。例如,在你的移动模块中,wear.xml文件可能包含以下代码,在 Wear 和移动模块中:
<resources>
<string-array name="android_wear_capabilities">
<item>verify_remote_example_phone_app</item>
</string-array>
</resources>
检测并引导用户安装相应的手机应用
Wear 2.0 引入了独立应用程序。Wear 应用足够强大,可以在没有移动应用支持的情况下运行。在必须要有移动应用的紧急情况下,Wear 应用可以指导用户安装移动支持应用和相应的 Wear 应用,通过以下步骤:
-
使用
CapabilityApi检查你的手机应用是否已安装在配对的手机上。更多信息,请查看谷歌提供的这个示例:github.com/googlesamples/android-WearVerifyRemoteApp. -
如果你的手机应用没有安装在手机上,使用
PlayStoreAvailability.getPlayStoreAvailabilityOnPhone()来检查它是什么类型的手机。 -
如果返回
PlayStoreAvailability.PLAY_STORE_ON_PHONE_AVAILABLE为true,表示手机中已安装 Play 商店。 -
在 Wear 设备上调用
RemoteIntent.startRemoteActivity(),使用市场 URI(market://details?id=com.example.android.wearable.wear.finddevices)在手机上打开 Play 商店。 -
如果返回
PlayStoreAvailability.PLAY_STORE_ON_PHONE_UNAVAILABLE,这意味着该手机很可能是 iOS 手机(没有 Play 商店)。通过在 Wear 设备上调用RemoteIntent.startRemoteActivity()并使用此 URI 打开 iPhone 上的 App Store:itunes.apple.com/us/app/yourappname。也请参阅从手表打开 URL。在 iPhone 上,从 Android Wear,你无法编程确定你的手机应用是否已安装。作为最佳实践,为用户提供一种机制(例如,一个按钮),以手动触发打开 App Store。
若要更详细地了解独立应用,请查看以下链接:独立应用介绍
在 Wear 设备上保持应用活跃
当我们为不同的使用场景编写应用时,需要做出一些调整。我们知道,在不使用应用时,应该让应用在 Wear 设备上进入休眠状态,以获得更好的电池性能;但是,当我们为地图构建应用时,有必要让地图对用户可见且处于活跃状态。
Android 为此提供了一个简单的配置:一个激活环境模式的几行代码方法:
//oncreate Method
setAmbientEnabled();
这将在地图上启动环境模式。当用户不再积极使用应用时,API 切换到非交互式和低色彩渲染的地图:
@Override
public void onEnterAmbient(Bundle ambientDetails) {
super.onEnterAmbient(ambientDetails);
mMapFragment.onEnterAmbient(ambientDetails);
}
下面的代码在 WearMap 上退出了环境模式。当用户开始积极使用应用时,API 切换到地图的正常渲染:
@Override
public void onEnterAmbient(Bundle ambientDetails) {
super.onEnterAmbient(ambientDetails);
mMapFragment.onEnterAmbient(ambientDetails);
}
为你的应用配置 WAKE_LOCK
当一些 Wear 应用始终可见时,它们非常有用。让应用始终可见会影响电池寿命,因此在你添加此功能到应用时,应仔细考虑这一影响。
在清单文件中添加 WAKE_LOCK 权限:
<uses-permission android:name="android.permission.WAKE_LOCK" />
WAKE_LOCK mechanism:
// Schedule a new alarm
if (isAmbient()) {
// Calculate the next trigger time
long delayMs = AMBIENT_INTERVAL_MS - (timeMs %
AMBIENT_INTERVAL_MS);
long triggerTimeMs = timeMs + delayMs;
mAmbientStateAlarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTimeMs,
mAmbientStatePendingIntent);
} else {
// Calculate the next trigger time for interactive mode
}
用户可以使用语音输入,而不是使用输入法框架读取输入,这需要在你的 Wear 设备上保持网络活跃:
private static final int SPEECH_REQUEST_CODE = 0;
// Create an intent that can start the Speech Recognizer activity
private void displaySpeechRecognizer() {
Intent intent = new
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
// Start the activity, the intent will be populated with the speech text
startActivityForResult(intent, SPEECH_REQUEST_CODE);
}
// This callback is invoked when the Speech Recognizer returns.
// This is where you process the intent and extract the speech text from the intent.
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == SPEECH_REQUEST_CODE && resultCode == RESULT_OK)
{
List<String> results = data.getStringArrayListExtra(
RecognizerIntent.EXTRA_RESULTS);
String spokenText = results.get(0);
// Do something with spokenText
}
super.onActivityResult(requestCode, resultCode, data);
}
了解完全交互模式和精简模式
Google Maps 安卓 API 可以作为精简模式地图提供静态图片。
为 Android 地图添加精简模式与配置正常地图类似,因为它将使用相同的类和接口。我们可以通过以下两种方式设置 Google 地图为精简模式:
-
作为
MapView或MapFrgament的 XML 属性 -
使用
GoogleMapOptions对象
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:name="com.google.android.gms.maps.MapFragment"
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
map:cameraZoom="13"
map:mapType="normal"
map:liteMode="true"/>
或者,按照以下方式使用 GoogleMapOptions 对象:
GoogleMapOptions options = new GoogleMapOptions().liteMode(true);
交互模式允许应用使用所有的生命周期方法,包括 onCreate()、onDestroy()、onResume() 和 onPause(),以及所有 Google API 功能,使应用完全交互。相应的代价是会有网络依赖。
有关交互模式和精简模式的更多信息,请查看以下链接:交互与精简模式
概述
我们已经来到了章节的末尾,期待着对 WearMapDiary 应用进行改进。现在,我们了解了如何创建一个MapsActivity,设置地图和 Google API 密钥,配置 Wear 模拟器中的 Google Play 服务,运行时权限检查,检查 GPS 硬件,以及使用geocoder类获取位置名称。我们已经理解了地图的交互模式和 Lite 模式的概念。在下一章中,让我们进一步了解 Wear 和地图用户界面控件以及其他 Google 地图技术,例如街景,更改地图类型等等。