Android Wear 按需环境模式





0/5 (0投票)
让用户可以选择在设备休眠时保持您的智能手表应用开启。
引言
2015 年 5 月,Google 对 Android Wear 进行了令人兴奋的更改,为第三方应用提供了环境模式支持。
操作系统一直以来都有环境模式:这是在用户不活动一段时间后,可穿戴设备会返回到的低功耗状态。但过去,如果您的应用在手表进入环境模式时打开,该应用就会关闭(并且表盘会再次显示)。
新的“应用环境模式”意味着您可以标记您的应用以在手表进入环境模式后继续显示在屏幕上。这是 Google 对智能手表以“一目了然”信息为中心的愿景的逻辑延伸:如果您有一个应用,用户可能需要查看比交互模式超时时间更长的时间,那么您现在可以为其启用环境模式,它将一直显示,直到用户手动关闭。可穿戴设备的核心是快速轻松地访问信息 - 此功能将这一理念推进一步,将智能手表的固有便利性扩展到第三方应用。
按需环境模式
在 Google 的设想中,环境模式是全有或全无的 - 要么它对您的应用有意义,要么没有。您在设计时就做出了决定,如果答案是是,那么您就将此行为融入您的应用。一个很好的例子是 Wear 上的默认计时器应用:如果您启动一个(例如)十分钟的计时器,该应用会在计时器运行时以环境模式显示在屏幕上,而不是在三十秒后变暗回到您的表盘。计时器完成后,应用就结束了 - 在环境模式中的显示也随之结束。
但是,如果您的应用需要比这更多的灵活性呢?如果您无法在设计时预测用户是否想要环境模式,或者不想要呢?我遇到了像这样的情况,我的Wearable Widgets 应用:根据用户选择在手表上放置的哪个小部件,环境模式可能有用,也可能无用。我根本无法预测。
一个显而易见的解决方案可能是将其内置到某个设置屏幕中,允许用户打开或关闭环境模式。我最初就是这样做的。但在测试这个实现时,我发现它很麻烦且效率低下;我会在小部件之间即时切换,而不得不返回一个设置屏幕来切换环境模式是一件很痛苦的事情。
总之,我想要的是按需环境模式。在任何给定时刻,能够告诉手表保持其当前内容在屏幕上,直到我告诉它停止。所以我构建了一个用户界面来实现这一点 - 在本文中,我将帮助您将此功能添加到您自己的 Android Wear 应用中。
先决条件:我在这里假设您有一些构建 Android 应用的经验,并且最好有一点开发 Android Wear 的经验。网上有很多好的教程可以帮助您入门,但让初学者全面了解所有内容超出了本文的范围。
用户界面设计
我实现按需环境模式的设想基于长按。这是 Android UI 设计中用于显示附加选项的经典手势,这非常适合我添加环境选项的计划。
特别是在 Android Wear 上,长按手势通常建议用于提供退出操作,以关闭应用。这意味着大多数编写良好的 Wear 应用不应将其他功能绑定到长按;换句话说,此手势不应与应用中的现有功能发生冲突。
当然,除了退出操作。如何将我的环境模式操作与用户在应用中预期长按后的退出行为集成在一起?
再次,我以 Android Wear 的整体 UI 设计为参考:并行操作通常显示为全屏操作按钮,通过水平滑动在它们之间移动。由于标准的退出 UI 实际上已经是一个操作按钮,所以这很自然:只需为环境模式添加第二个“按钮”。
有了这个整体设计,我只需要一个图标来表示我的环境模式操作按钮。这变成了一个小挑战,因为我找不到与环境模式相关的标准化图像;作为一个概念,这是 Google 为 Android Wear 创建的,并且他们似乎还没有发现它需要图形化。
经过一番搜索,我找到的最接近的类比是标准的暂停图标,用于将计算机发送到低功耗待机模式。环境模式与待机模式不完全相同,但我认为在用户可以识别的标准图标范围内,这可能是我所能希望的最好匹配。
我不是一个伟大的艺术家,但像这样的简单线条和圆圈设计即使在我能力范围内,并且用GIMP 花了几分钟后,我就创建了一个(或多或少)符合 Android Wear 操作按钮规范的待机图标版本。您稍后会在截图中看到它。
这样,我就准备开始实现了。让我们写一些代码吧!
WearActionButton 类
将设计转换为工作应用组件的第一步是构建所需的布局。对于这个项目,实际上只有一个:全屏操作按钮,如上面的退出截图所示。
理想情况下,我们希望它尽可能地看起来像 Wear 系统自己的操作按钮 - 但在创建此实现时,我发现 Wear 的操作按钮并未得到充分标准化。Google 已经制定了设计指南,但系统生成的操作按钮(例如,附加到通知上的按钮)并不完全遵循它们。
我来回考虑了这个问题,花了一些时间尝试复制系统按钮,但最终还是决定遵循发布的指南。在很大程度上,是因为这个 StackOverflow 回答,它提供了一个功能齐全的布局文件,用于符合指南的操作按钮。您只需将其中的两个 XML 文件保存到您自己的 res 子目录中(它们也包含在此文章附带的 ZIP 文件中)。
请注意,上面链接的 SO 回答包含一个 Fragment 来实现操作按钮本身。对于我的实现,Fragment 并不是最佳选择,所以我将该代码改编成了一个简单的 LinearLayout 子类。这是:
public class WearActionButton extends LinearLayout {
    private CircledImageView vIcon;
    private TextView vLabel;
    public WearActionButton(Context context) {
        this(context, null, 0, 0);
    }
    public WearActionButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        LayoutInflater.from(context).inflate(R.layout.wear_action_button, this);
        vIcon = (CircledImageView) findViewById(R.id.icon);
        vLabel = (TextView) findViewById(R.id.label);
    }
    public void setIcon(int iconResId) {
        vIcon.setImageResource(iconResId);
        if (iconResId == R.drawable.ic_full_cancel) {
            // Override the default blue button color with red (for Exit)
            vIcon.setCircleColorStateList(
                    getContext().getResources().getColorStateList(R.color.dismiss_button));
        }
    }
    public void setLabel(int labelResId) {
        vLabel.setText(labelResId);
    }
}
它真的很简单。在构造函数中,它将 wear_action_button.xml 文件注入到基础 LinearLayout 中,并初始化两个字段供以后使用。它还声明了几个公共 set 方法供包含类使用,我们将在下一节中看到。
要使 WearActionButton 工作,您只需要再一个文件,那就是 dismiss_button.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#ff5151" android:state_pressed="false" />
    <item android:color="#b83120" android:state_pressed="true" />
</selector>
默认情况下,操作按钮的颜色为蓝色,在 StackOverflow 回答的 action_button.xml 中声明。此文件仅为退出(关闭)按钮的红色圆盘设置了等效的颜色选择器。
OverlayView 类
现在我们准备好将我们的 WearActionButton 组装成所需的水平均滑容器。Android 有一个框架类 ViewPager,它相当适合这种模式 - 但要使其按我们想要的方式工作需要一些工作。我将我的 ViewPager 后代命名为 OverlayView,因为它覆盖了父 Activity 中的其他内容。
[注意:为了清晰起见,我将在各个部分介绍 OverlayView。完整列表请参见附带的 ZIP 文件。]
我们从一些非常基础、几乎是样板代码开始,初始化类并声明其字段
public class OverlayView extends ViewPager {
    private WearActionButton ambientView, dismissView;
    private OverlayAdapter adapter;
    public OverlayView(final Context context, AttributeSet attrs) {
        super(context, attrs);
        setBackgroundResource(android.support.wearable.R.color.dismiss_overlay_bg);
        setClickable(true);
        setVisibility(GONE);
    }
}
不出所料,我们有两个 WearActionButton,分别用于环境模式和退出(关闭)操作。另请注意,OverlayView 默认是不可见的;我们将在下一节中使其可见。
如果您已经进行了大量的 Android 开发,您很可能使用过某种形式的 Adapter。Adapter 是 Android 中的一个基本模式,它构成了几乎所有由重复项组成的 UI(如列表)。这个描述 - 一个由重复项组成的 UI - 也适用于 ViewPager,所以它采用 Adapter 驱动并不奇怪。
这里的驱动程序称为 PagerAdapter,所以这就是我们需要继承以使我们的 ViewPager 工作的内容。我将我的类命名为 OverlayAdapter
public class OverlayAdapter extends PagerAdapter {
   @Override
   public int getCount() {
       return 3;
   }
   @Override
   public Object instantiateItem(ViewGroup viewGroup, int page) {
       switch (page) {
           case 1:
               if (ambientView == null) {
                   // Initialize the "Enable Ambient" button
                   ambientView = new WearActionButton(getContext());
                   ambientView.setIcon(R.drawable.ic_full_standby);
                   ambientView.setLabel(R.string.enable_ambient);
                   ambientView.setOnClickListener(new OnClickListener() {
                       public void onClick(View v) {
                          setAmbient();
                       }
                   });
                   viewGroup.addView(ambientView);
               }
               return ambientView;
           case 2:
               if (dismissView == null) {
                   // Initialize the "Exit" button
                   dismissView = new WearActionButton(getContext());
                   dismissView.setIcon(R.drawable.ic_full_cancel);
                   dismissView.setLabel(R.string.exit);
                   dismissView.setOnClickListener(new OnClickListener() {
                       public void onClick(View v) {
                           if (getContext() instanceof Activity) {
                               ((Activity) getContext()).finish();
                           }
                       }
                   });
                   viewGroup.addView(dismissView);
               }
               return dismissView;
       }
       return null;
   }
   @Override
   public void destroyItem(ViewGroup viewGroup, int page, Object o) {
       switch (page) {
           case 1:
               ambientView = null;
               break;
           case 2:
               dismissView = null;
               break;
       }
   }
   @Override
   public boolean isViewFromObject(View view, Object obj) {
       return (view == obj);
   }
}
如果您以前使用过任何 Adapter,基本方法 - getCount、instantiateItem 等 - 应该看起来很熟悉。如果您没有,我建议您熟悉平台这一区域;在您的 Android 职业生涯中,您总会需要它。因此,我不会详细介绍这个类的“无聊”部分。
这个类中有趣的部分在 instantiateItem 方法中。在那里 - 根据选择了哪个页面 - 我们创建一个 WearActionButton,为其初始化环境模式或退出,并将其添加到包含的 ViewGroup 中。

我们还为每个操作按钮设置了一个 onClick 侦听器。退出(关闭)按钮的侦听器会执行您期望的操作:关闭父 Activity。环境模式按钮的侦听器会调用一个名为 setAmbient 的方法。 
private void setAmbient(WearWidgetActivity activity) {
    final WearableActivity activity;
    if (getContext() instanceof WearWidgetActivity) {
        activity = ((WearableActivity) getContext());
        // Show confirmation to the user
        Intent intent = new Intent(activity, ConfirmationActivity.class);
        intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,
                ConfirmationActivity.SUCCESS_ANIMATION);
        intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,
                activity.getString(R.string.ambient_enabled));
        activity.startActivity(intent);
        // Hide the OverlayView (after a short delay to avoid flicker)
        new Handler() {
            @Override
            public void handleMessage(Message msg) {
                setVisibility(GONE);
            }
        }.sendEmptyMessageDelayed(0, 250);
        // Turn on ambient mode for the parent activity
        activity.setAmbientEnabled();
    }
}
这就是这一切,我们期待已久的时刻
- 
	我们启动一个标准的 Wear确认活动,让用户知道此应用已启用环境模式。 
- 
	简单的 Handler 只是隐藏了 OverlayView,它的工作已经完成。
- 最后,调用 setAmbientEnabled会告诉 Wear 框架在此Activity退出交互模式后保持其可见。
所以,我们似乎完成了 OverlayView - 但在我们继续之前,还有最后一件事需要提到。
如果您密切关注了上面的 OverlayAdapter 列表,您可能会注意到一些奇怪的地方:getCount 方法返回 3。但我们只有两个页面要显示,环境模式和退出。这是怎么回事?
答案需要一点解释。回想一下,我们的设计基于推荐的退出按钮模式 - 该模式的另一个方面是,它应该可以通过从左到右滑动来关闭,以防用户长按但后来决定不退出应用。本质上,用户可以“滑动掉”退出按钮 - 这是 Wear 中的一个标准手势。
当我第一次创建我的 OverlayView 时,我花了很长时间来寻找一种清晰的方法让用户滑动掉它。我发现 ViewPager 消耗了大多数滑动手势,并且不允许我向一个手势附加一个“关闭”操作。
所以,我找到了一个技巧来实现相同的效果:我创建了一个空白的第一个页面 - 在环境模式操作的左侧 - 当用户滑动到它时会隐藏覆盖层。除了上面在 OverlayAdapter 中已经看到的代码之外,还需要另外几段代码来实现这一点,这些代码会放入 OverlayView 的 setVisibility 方法中。
@Override
public void setVisibility(int visibility) {
   if (visibility == VISIBLE) {
       adapter = new OverlayAdapter();
       setOnPageChangeListener(new OnPageChangeListener() {
           @Override
           public void onPageSelected(int page) {
               if (page == 0) {
                   // Blank first page selected - hide the overlay
                   setVisibility(GONE);
               }
           }
           // Required by interface, not needed in this implementation
           @Override
           public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
           @Override
           public void onPageScrollStateChanged(int i) {}
       });
   } else {
       setOnPageChangeListener(null);
       adapter = null;
   }
   setAdapter(adapter);
   setCurrentItem(1);    // skip the first (blank) page when initializing the adapter
   super.setVisibility(visibility);
}
首先,请注意 OnPageChangeListener。它的工作很简单:当 OverlayView 更改为页面 0 - 空白的第一页 - 时,隐藏视图。这是当用户将环境模式操作按钮向右滑动时出现的“关闭”操作。
其次,在此方法的最后部分有一个调用 setCurrentItem(1)。这确保了 OverlayView 始终打开到第一个可见页面,跳过空白的第一页。
至此,我们完成了 OverlayView 类以及我们的大部分工作。现在真正剩下的是当用户在包含的 Activity 上长按时将其显示给用户。
检测长按
Android 框架在 GestureDetector 类中为开发人员提供了相当强大的手势检测功能,因此这是检测 Wear 应用中长按的明显途径。如果您开发过任何形式的标准手势 UI(长按、单击、双击等)的 Android 应用 - 您可能已经使用过 GestureDetector。在 Wear 上也没有什么不同,但我将带您了解此项目的具体细节。
实现 GestureDetector 的实例实际上是一个两步过程,还涉及到 GestureListener(特别是 SimpleGestureListener 非常适合我们的需求)。您首先需要为每个声明一个类级字段,如下所示:
private GestureDetector gestureDetector;
private SimpleOnGestureListener gestureListener;
然后,您需要实例化这两个变量。一个不错的地方是在您 Activity 的 onCreate 方法中执行此操作,但您可以根据自己代码的需要将其移至其他地方。
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    gestureDetector = new GestureDetector(this, gestureListener);
    gestureListener = new SimpleOnGestureListener() {
        public void onLongPress(MotionEvent e) {
            actionOverlay = (OverlayView) findViewById(R.id.action_overlay);
            actionOverlay.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
            actionOverlay.setVisibility(View.VISIBLE);
        }
    }
}
GestureDetector 有一个简单的单行构造函数,但 SimpleGestureListener 也需要定义 onLongPress 处理程序,如下所示。当用户长按时,我们会给他们一些触觉反馈(振动),让他们知道手势已注册。然后,我们创建并显示 OverlayView,如前一节所述。
现在我们的手势处理程序已准备就绪,但为了让用户能够与其交互,我们需要将其附加到包含的 Activity。同样,这只是非常基本的 Android 样板代码。
@Override
public boolean onTouch(View view, MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
}
通过重写 Activity 的 onTouch 处理程序,我们可以将触摸事件转发给 GestureDetector,它将负责决定该触摸是否为长按。
在我们完成之前,还有最后一个收尾工作。如果 OverlayView 已显示,但在用户与之交互之前包含的 Activity 消失了(例如,他们用手掌覆盖屏幕以返回表盘),我们希望确保下次显示 Activity 时 OverlayView 不再可见。我们通过在 Activity 的 onStop 方法中将其隐藏来做到这一点。
@Override
protected void onStop() {
    if (actionOverlay != null) {
        actionOverlay.setVisibility(View.GONE);
    }
    super.onStop();
}
最后的想法
现在,您就可以让您的 Wear 用户在设备进入环境模式时保持您的应用运行。不过,在您开始之前,我还需要提到几件事。
第一个是关于此技术如何工作的观察。与在应用设置中放置“环境模式”选项不同,我的方法是一次性操作。无论是否处于环境模式,用户在想要离开时都会关闭 Activity - 下次打开时,环境模式将恢复为禁用状态。
在我看来,这是一个好设计。让应用在手表进入环境模式时运行可能会比不这样做消耗更多电量 - 而且由于其电池非常小,因此在智能手表上,功耗比大多数其他设备更关键。所以,如果您赋予用户此控制权,我相信环境模式的使用通常应该由他们每次有意识地决定。
这就引出了一个(希望是显而易见的)关于环境模式下应用的问题:您应该采取一切可能的措施来最大限度地降低其功耗。通常,这意味着降低更新速率,或者可能是显示刷新速率。具体取决于您的应用 - 但不要忽视这个问题,否则您的用户会惩罚您。
此外,还有一些其他问题,涉及显示颜色深度、烧屏保护等。Google 在 Android Wear 开发者文档中很好地概述了这些问题。我强烈建议您阅读并应用这些建议。
最后,您可能想知道在哪里实现所有这些建议?请记住,我上面提供的代码都是在您的应用真正进入环境模式之前运行的:用户已表示他们想要启用环境模式,但直到他们将手表闲置足够长的时间屏幕才会超时,它才真正发生。
幸运的是,平台为启用了环境模式的活动提供了几个方法,让您知道何时发生转换。它们分别命名为onEnterAmbient 和 onExitAmbient;名称非常不言自明,但如果您需要更多详细信息,请参阅文档。
这样,您就可以为您的 Wear 应用添加环境模式选项了。如果您想分享应用中应用了此技术的应用,或者有任何疑问,请在下面的评论中告诉我们!
历史
2015 年 10 月 9 日:首次发布


