Android Wear 演示应用程序 - TodayMenu
一切都关于 Android Wear 演示应用程序“TodayMenu”的实现
引言
在本文中,我将引导您完成一个名为“TodayMenu”的 Android Wear 演示应用程序。我将重用“Martin Knudsen”开发的示例代码。应用程序开发的全部功劳归 Martin Knudsen。
https://github.com/zaifrun/TodayMenu
这个项目是关于今日菜单的,作者将其开发为学生的学习材料。
该示例项目利用了一些最常用的 Android Wear 组件。因此,了解一些 Android 小部件(如 BoxInsetLayout、WearableListView、FrameLayout、LinearLayout 等)会很有帮助。
TodayMenu 在四个片段中显示屏幕。主片段显示选项列表,即食物项。第二个片段显示所选食物项的统计信息。第三个片段接受语音输入,即食物项名称,并将其暂时保存在 SQLite 数据库中。最后一个片段显示两个按钮,一个用于重置统计信息,另一个用于重置食物选择。
让我们看看 SQLite 代码,了解应用程序如何将数据保存在 SQLite 数据库中。以下是您可以导航和学习的主题。
- TodayMenu 应用 UI
- 数据库自定义类
- MainActivity XML 和代码
- GridViewPager Adaper
- 菜单片段
- 菜单片段 ListView 绑定
- 主片段重置选择列表
- 统计片段
- 更新统计 UI
- 语音片段
- 处理语音片段的 onClick 事件
- 清除片段
- 在智能手表上调试
背景
请查看以下链接以了解 Android Wear。
https://codeproject.org.cn/Articles/1038337/Introduction-to-Android-Wear
TodayMenu 应用 UI
在深入了解 TodayMenu 应用程序的功能之前,让我们快速浏览一下应用程序的 UI 屏幕。
数据库自定义类
我们有一个名为 Database 的类,它继承自 SQLiteOpenHelper
。我们覆盖了两个方法,即 onCreate
和 onUpdate
。下面是 onCreate
重写的代码片段。我们正在执行 SQL 脚本来创建菜单和选择表。
@Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE menu ( id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT, weekday INTEGER);"); db.execSQL("CREATE TABLE choices ( id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT);"); }
这是 onUpgrade
重写的代码片段。如果旧版本是 1,新版本是 2,那么我们正在执行 SQL 脚本来创建用于用户定义选择的新表。onUpgrade
根据应用程序版本执行。如果您想为需要数据库更改的应用程序添加功能,这里是您可以处理的地方。
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion==1 && newVersion==2) db.execSQL("CREATE TABLE choices ( id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT);"); }
这个名为 'Database
' 的类还有其他一些处理读取选择、添加食物等的方法。我们很快就会看一下。
MainActivity XML 和代码
现在让我们看一下主 activity xml
和关联的代码。下面是 activity.xml
的代码片段。您会注意到使用了 BoxInsetLayout
,因此可以在圆形或方形手表上显示相同的 UI。它包含了 GridViewPager
和 DotsPageIndicator
。
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="match_parent" android:layout_width="match_parent"> <!-- This is the gridviewpager, it makes sure we can swipe between different views --> <android.support.wearable.view.GridViewPager xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- This is the DotsPageIndicator, it makes sure we can use the small dots on the bottom of the screen to indicate the current page of the app is displayed --> <android.support.wearable.view.DotsPageIndicator android:id="@+id/page_indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal|bottom"> </android.support.wearable.view.DotsPageIndicator> </android.support.wearable.view.BoxInsetLayout>
下面是主活动 onCreate
重写的代码片段。我们将深入了解 GridViewPager
如何与数据进行设置。您可以在下面看到,如何将 SampleGridPagerAdapter
实例设置为活动的 GridViewPager
。此外,DotsPageIndicator
也与 pager 实例一起设置,因此它会提供有关用户当前所在片段的视觉反馈。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); final Resources res = getResources(); final GridViewPager pager = (GridViewPager) findViewById(R.id.pager); pager.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() { @Override public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { final boolean round = insets.isRound(); int rowMargin = res.getDimensionPixelOffset(R.dimen.page_row_margin); int colMargin = res.getDimensionPixelOffset(round ? R.dimen.page_column_margin_round : R.dimen.page_column_margin); pager.setPageMargins(rowMargin, colMargin); pager.onApplyWindowInsets(insets); return insets; } }); pager.setAdapter(new SampleGridPagerAdapter(this, getFragmentManager())); pagerGlobal = pager; DotsPageIndicator dotsPageIndicator = (DotsPageIndicator) findViewById(R.id.page_indicator); dotsPageIndicator.setPager(pager); Database db = new Database(this); db.readChoices(); //when the app starts we need to read the choices from the database file }
主活动加载时会发生一些有趣的事情。即,我们也会读取选择。这是处理读取选择的代码片段,它位于 Database
类中。首先,我们发出一个 SELECT 查询以按 id 顺序获取所有选择。如果计数为 0,表示没有用户输入的选择,那么我们将遍历选择列表并插入到我们的选择表中。否则,我们将遍历选择,收集所有选择并将它们设置为一个静态字符串数组 Choices.ELEMENTS
。
public String[] readChoices() { SQLiteDatabase database = getReadableDatabase(); Cursor cursor = database.rawQuery("SELECT name FROM choices ORDER BY id",null); int count = cursor.getCount(); if (count==0) //there is nothing, so first time we start the app. { for (String choice : Choices.ELEMENTS_RESET) database.execSQL("INSERT INTO choices (name) VALUES ('"+choice+"')"); Choices.ELEMENTS = new String[Choices.ELEMENTS_RESET.length]; System.arraycopy(Choices.ELEMENTS_RESET, 0, Choices.ELEMENTS, 0, Choices.ELEMENTS_RESET.length); cursor.close(); return Choices.ELEMENTS_RESET; //we just have default values. } else { String[] elements = new String[count]; int index = 0; while (cursor.moveToNext()) { elements[index] = cursor.getString(0); index++; } Choices.ELEMENTS = elements; //overwrite the elements array cursor.close(); return elements; } }
这是 Choices
类的代码片段。我们有初始的选择列表以及用户定义的或输入的选择。
public class Choices { public static String[] ELEMENTS_RESET = { "Chicken", "Beef", "Pork", "Lamb","Duck","Turkey" }; public static String[] ELEMENTS; }
现在是时候看看 SampleGridPagerAdapter
的逻辑了。它是一个自定义类,继承自 FragmentGridPagerAdapter
。下面是相同的代码片段。目前,我们处理四个片段。在构造函数中,我们创建将要在 GridViewPager
上显示的每个片段的新实例。有一个重写的方法 getFragment
,您可以在下面看到,它根据列返回片段实例。当用户从左向右滑动时,这些片段就会显示在可穿戴设备上。
GridViewPager Adaper
下面是 GridViewPager Adapter 的代码片段。
public class SampleGridPagerAdapter extends FragmentGridPagerAdapter { MenuFragment menuFragment; ClearFragment clearFragment; StatsFragment statsFragment; SpeechFragment speechFragment; public SpeechFragment getSpeechFragment() { return speechFragment; } public ClearFragment getClearFragment() { return clearFragment; } public StatsFragment getStatsFragment() { return statsFragment; } public MenuFragment getMenuFragment() { return menuFragment; } public SampleGridPagerAdapter(Context ctx, FragmentManager fm) { super(fm); menuFragment = new MenuFragment(); clearFragment = new ClearFragment(); statsFragment = new StatsFragment(); statsFragment.setContext(ctx); //we need the context in this class speechFragment = new SpeechFragment(); } public void notifyStatsSetChanged() { statsFragment.updateUI(); } public void listViewDataSetChanged() { menuFragment.resetList(); } @Override public Fragment getFragment(int row, int col) { if (col==0) return menuFragment; else if (col==1) return statsFragment; else if (col==2) return speechFragment; else return clearFragment; } @Override public int getRowCount() { return 1; //we just have 1 row of pages, meaning scrolling horizontally } @Override public int getColumnCount(int rowNum) { return 4; //we just have 4 columns - fixed in this app. } }
现在让我们深入了解上述每个片段,以了解更多关于内部工作的信息。下面是 MenuFragment
的代码片段,它继承自 Fragment
并实现了 WearableListView.ClickListener
。onCreateView 重写包含一个代码来膨胀布局,以便我们找到 WearableListView
并使用选项列表设置其适配器。
菜单片段
这是菜单片段的代码片段。
public class MenuFragment extends Fragment implements WearableListView.ClickListener { WearableListView listView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.select, container, false); listView =(WearableListView) view.findViewById(R.id.wearable_list); if (listView!=null) { listView.setAdapter(new Adapter(getActivity().getApplicationContext(), Choices.ELEMENTS)); // Set a click listener - using this activity listView.setClickListener(this); listView.setGreedyTouchMode(true); } return view; } …. }
下图是“TodayMenu”应用主屏幕的快照。您可以看到它显示了包含 ListView
的菜单片段。
这是我们处理可穿戴列表视图 onClick
事件的代码片段。
1) 首先,我们需要获取所选列表视图项的索引,我们从一个“Tag
”对象中获取该索引。接下来,您将看到关于如何设置标签的详细信息。
2) 根据标签值获取食物选择。
3) 创建 Database
类的实例并调用 addFood
,以便我们可以保存我们的选择。
4) 创建一个 Intent 实例,向用户显示成功确认。
5) 最后,我们将更新统计片段 UI,以便当用户导航时,她/他可以看到用户所选内容已更新的统计信息。
@Override public void onClick(WearableListView.ViewHolder v) { Integer tag = (Integer) v.itemView.getTag(); int index = tag.intValue(); String chosen = Choices.ELEMENTS[index]; Database db = new Database(getActivity()); db.addFood(chosen); Intent intent = new Intent(getActivity().getApplicationContext(), ConfirmationActivity.class); intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.SUCCESS_ANIMATION); intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,getResources() .getString(R.string.saved)+" "+chosen); startActivity(intent); ((SampleGridPagerAdapter) MainActivity.getPager().getAdapter()).notifyStatsSetChanged(); }
菜单片段 ListView 绑定
下面是菜单片段列表视图适配器的代码片段。适配器接受两个参数,一个是上下文,另一个是数据集实例。数据集包含所有要显示的选项列表。我们需要关注两个主要的重写方法。即 onCreateViewHolder
和 onBindViewHolder
。
在 onCreateViewHolder
方法中,我们所要做的就是返回一个 WearableListView.ViewHolder
实例。
使用视图创建一个 ItemViewHolder
的新实例。LayoutInflator
实例用于膨胀布局 R.layout.list_item
。onBindViewHolder
方法在内部被调用,在那里我们得到 ViewHolder
实例,然后得到 TextView
,以便我们可以从数据集中的位置设置适当的文本。您还会注意到我们调用了设置标签对象的值为位置,以便我们可以在 onClick
事件中使用它,从而获得正确的选择并将其保存在数据库中。
private static final class Adapter extends WearableListView.Adapter { private String[] mDataset; private final Context mContext; private final LayoutInflater mInflater; public Adapter(Context context, String[] dataset) { mContext = context; mInflater = LayoutInflater.from(context); mDataset = dataset; } public static class ItemViewHolder extends WearableListView.ViewHolder { private TextView textView; public ItemViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.name); } } @Override public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ItemViewHolder(mInflater.inflate(R.layout.list_item, null)); } @Override public void onBindViewHolder(WearableListView.ViewHolder holder, int position) { ItemViewHolder itemHolder = (ItemViewHolder) holder; TextView view = itemHolder.textView; view.setText(mDataset[position]); holder.itemView.setTag(position); } @Override public int getItemCount() { return mDataset.length; } }
主片段重置选择列表
现在让我们看看如何重置选项列表。下面是相应的代码片段。首先,我们需要获取选项长度并将所有选项暂时复制到字符串数组中,以便我们可以通过设置适配器并调用 invalidate 方法来刷新 Choices.ELEMENTS
和 ListView
组件。
public void resetList() { int len = Choices.ELEMENTS_RESET.length; String[] newElements = new String[len]; System.arraycopy(Choices.ELEMENTS_RESET, 0, newElements, 0, len); Choices.ELEMENTS = newElements; listView.setAdapter(new Adapter(getActivity().getApplicationContext(), Choices.ELEMENTS)); listView.invalidate(); }
统计片段
现在让我们看看 StatsFragment
,它显示用户选择的食物选项的详细统计信息。下面是 StatsFragment 的部分代码片段。我们使用一个 LinearLayout
,其中有一个 TextView
,文本设置为“Statistics”。接下来,我们将看到如何将另一个 TextView
组件添加到 LinearLayout
中,以便我们可以向用户显示实际统计信息。
<LinearLayout android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="center" android:orientation="vertical" android:id="@+id/statslayout"> <TextView android:layout_height="wrap_content" android:layout_width="wrap_content" android:gravity="center_horizontal" android:textSize="24sp" android:layout_gravity="center_horizontal" android:textColor="@color/blue" android:text="@string/statistics"/> </LinearLayout>
这是统计片段的快照。
更新统计 UI
现在是时候看看统计信息是如何显示给用户的了。在 StatsFragment
onCreate
重写中,我们调用了 update UI。下面是相应的代码片段。这是我们所做的。
1) 创建 Database
的实例并从 SQLite DB 获取最新统计信息。将其保存在 Item
类型的 ArrayList 中。
2) 获取 LinearLayout
实例的子项计数,以便我们可以删除并添加视图,让用户看到最新的刷新视图。
3) 接下来的几行,我们遍历所有统计信息,创建一个 TextView
实例,然后设置文本、颜色、字体等,并将其添加到 LinearLayout
实例中。
public void updateUI() { Database db = new Database(context); ArrayList<Item> items = db.getStats(); Collections.sort(items); //items are now sorted according to the frequency int children = parent.getChildCount(); if (children>1) //do we have more than the "statistics" label, then remove them { parent.removeViews(1, children-1); } for (Item item : items) { TextView text = new TextView(getActivity()); String p = String.format("%.1f", item.getPercent()); text.setText(item.getName() + " : "+item.getFreq()+ " ("+p+" %)"); text.setTextColor(Color.WHITE); text.setTextSize(22); text.setLayoutParams(new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); parent.addView(text); //add the textview to the layout. } }
语音片段
让我们看看语音片段,看看它的内部工作。语音是 Android Wear 的重要组成部分,此片段的主要功能是接受用户的语音输入并将其保存为选择。语音片段实现了 onClickListener
,因此它可以处理用户点击。这是 onCreateView
重写的代码片段。
在 onCreateView
中,我们首先通过膨胀语音布局来获取视图实例。这样我们就可以找到语音和添加项按钮,并为它们附加 onClick
事件。也别忘了重置文本输入。
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.speech, container, false); Button button = (Button) view.findViewById(R.id.speechButton); button.setOnClickListener(this); button = (Button) view.findViewById(R.id.addItemButton); button.setOnClickListener(this); textView = (TextView) view.findViewById(R.id.speechText); textInput = ""; return view; }
这是语音片段的快照。
处理语音片段的 onClick 事件
让我们看看如何处理语音和添加项按钮的 onClick
事件。这是调用 displaySpeechRecognizer
方法的代码片段,以启动语音识别器以接受语音输入。
@Override public void onClick(View v) { if (v.getId()==R.id.speechButton) { displaySpeechRecognizer(); } else if (v.getId()==R.id.addItemButton) { if (textInput.length()>0) addData(); else { Toast toast = Toast.makeText(getActivity().getApplicationContext(), "No input to add",Toast.LENGTH_LONG); toast.show();; } } }
这是显示语音识别器的代码片段。我们必须创建一个具有适当 Intent 操作的 Intent 实例,以便可以使用该 Intent 和语音代码启动活动。
private void displaySpeechRecognizer() { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); startActivityForResult(intent, SPEECH_CODE); }
接收语音输入后,下一件重要的事情是添加输入文本。这是相应代码片段。首先,我们通过使用 Intent 来显示确认屏幕,但实际上我们是通过 MenuFragment
添加文本输入。
请注意 - 可以在语音片段本身中添加新选择。但我们不应忘记刷新 MenuFragment
ListView UI。
public void addData() { Intent intent = new Intent(getActivity(), ConfirmationActivity.class); intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.SUCCESS_ANIMATION); intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,getResources() .getString(R.string.choiceAdded)); startActivity(intent); MenuFragment frag = ((SampleGridPagerAdapter) MainActivity.getPager() .getAdapter()).getMenuFragment(); frag.addData(textInput); }
清除片段
清除片段是显示在 GridViewPager
上的最后一个片段。它继承自 Fragment
类并实现了 OnClickListener
。下面是 onCreateView
方法重写的代码片段,您可以在其中看到我们正在设置 onClickListerner
来处理用户点击。
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.clear, container, false); Button button = (Button) view.findViewById(R.id.clearDataButton); button.setOnClickListener(this); button = (Button) view.findViewById(R.id.clearChoicesButton); button.setOnClickListener(this); return view; }
这是清除片段的快照。
现在是时候看看 onClick
重写并尝试了解我们是如何实际处理清除数据和清除选择按钮点击的。在每个按钮点击时,我们都会显示一个自定义对话框,以便用户可以采取适当的行动。
MyDialogFragment
是一个自定义类,它继承自 DiaglogFragment
并重写 onCreateDialog
。它有两个名为 positiveClick
和 negativeClick
的方法,没有实现,允许覆盖。下面您可以看到我们如何通过调用 clearData 方法来处理 positiveClick
方法以清除数据。
@Override public void onClick(View v) { if (v.getId()==R.id.clearDataButton) { MyDialogFragment dialog = new MyDialogFragment() { @Override protected void positiveClick() { super.positiveClick(); clearData(); } @Override protected void negativeClick() { super.negativeClick(); } }; Bundle bundle = new Bundle(); bundle.putString("title",getResources().getString(R.string.deleteStatsTitle)); bundle.putString("message",getResources().getString(R.string.deleteStatsMessage)); dialog.setArguments(bundle); dialog.show(this.getFragmentManager(),"test"); //test is just a tag - not shown to the user } else if (v.getId()==R.id.clearChoicesButton) { DialogFragment newFragment = MyWearDialog.newInstance(); newFragment.show(getFragmentManager(), "dialog"); } }
现在是时候看看 clearData
代码并了解背后的代码了。在 Database 类中,我们实现了清除菜单项的功能,然后显示一个带有成功动画的确认活动。
最后,我们还有一件重要的事情要做,那就是——通过调用其适配器上的 notifyStatsSetChanged
方法来通知 SampleGridPagerAdapter
。
public void clearData() { Database db = new Database(getActivity()); db.clearData(); db.close(); Intent intent = new Intent(getActivity(), ConfirmationActivity.class); intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.SUCCESS_ANIMATION); intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE, getResources().getString(R.string.statsDeleted)); startActivity(intent); ((SampleGridPagerAdapter) MainActivity.getPager().getAdapter()).notifyStatsSetChanged(); }
现在让我们看看我们是如何处理清除选择按钮点击事件的。在清除选择按钮点击时,您可以看到有一个显示 DialogFragment
的代码。我们使用了 MyWearDialog
,它只是一个自定义对话框片段,因为它继承自 DiaglogFragment
。这是 MyWearDialog
的代码片段,它处理“OK”和“Cancel”按钮的点击事件。您可以在下面看到 cancel 只是关闭对话框。点击“OK”按钮会调用 clearChoices
,它使用 Database
实例来清除所有选择。
请注意 – 清除选择后,切勿忘记通过通知 GridViewPager
来刷新它。
@Override public void onClick(View v) { if (v.getId()==R.id.cancel_btn) { dismiss(); //just do nothing } else if (v.getId()==R.id.ok_btn) { clearChoices();//do something dismiss(); //then dismiss } } public void clearChoices() { Database db = new Database(getActivity()); db.clearChoices(); db.close(); ((SampleGridPagerAdapter)MainActivity.getPager().getAdapter()).listViewDataSetChanged(); Intent intent = new Intent(getActivity(), ConfirmationActivity.class); intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.SUCCESS_ANIMATION); intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,getResources() .getString(R.string.choicesDeleted)); startActivity(intent); }
在智能手表上调试
请查看以下文章,了解更多关于如何在可穿戴设备上调试应用程序的信息。
https://codeproject.org.cn/Articles/1034397/Android-Wear-through-ADB
参考文献
本文使用了“Martin Knudsen”开发的示例代码。请随时查看下面的 Github 链接。从文章开头就已全部归功于作者。
https://github.com/zaifrun/TodayMenu
关注点
Martin Knudsen 开发的示例应用程序在我理解如何开发 Android Wear 应用方面帮助很大。没有它,我将很难获得灵感并在 Android Wear 上进行编码。
有一件重要的事情需要提及。请参考并理解 Android 可穿戴设计指南,并对演示应用程序进行更改,使其成为可投入生产的应用。
历史
版本 1.0 - 文章及代码示例初次发布 - 2015/10/14。