Piasy · 更新于 2018-11-28 11:00:42

Best Practices for Interaction and Engagement

Designing Effective Navigation

  • Planning Screens and Their Relationships
    • 实体关系图(Entity-relationship diagram),数据、用户之间的关系、操作
    • 详尽的用例场景
    • 界面关系图
  • Planning for Multiple Touchscreen Sizes
    • 界面组合技术,在平板电脑、电视上,屏幕很大,可以把列表界面和详情界面左右排布一起显示
  • Providing Descendant and Lateral Navigation
    • Descendant,父界面前往子界面
    • Lateral,兄弟界面之间的跳转
    • list和section
  • Providing Ancestral and Temporal Navigation
    • Temporal(时间性),按下返回键,应该回到上一界面
    • Ancestral(父子性),ActionBar/ToolBar上的返回按钮,返回父界面(通常是上一界面,但并不全是,容纳WebView的Activity就是很好的例子),需要注意的是,一定要清除backstack
  • ...

Implementing Effective Navigation

  • Creating Swipe Views with Tabs
    • ViewPagerPagerTitleStripFragmentPagerAdapterFragmentStatePagerAdapter
  • Creating a Navigation Drawer
    • drawer layout里面可以显式一个菜单列表
    • ActionBarDrawerToggle(appcompat-v7中)可以监听drawer的开启与关闭事件,也可以用代码控制drawer的开启与关闭
    • 完整样例
  • Providing Up Navigation

    • 首先在manifest里面为activity声明其parent activity,用于up navigation
    • 然后在onOptionsItemSelected回调中处理up navigation:
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
          switch (item.getItemId()) {
          // Respond to the action bar's Up/Home button
          case android.R.id.home:
              Intent upIntent = NavUtils.getParentActivityIntent(this);
              if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
                  // This activity is NOT part of this app's task, so create a new task
                  // when navigating up, with a synthesized back stack.
                  TaskStackBuilder.create(this)
                          // Add all of this activity's parents to the back stack
                          .addNextIntentWithParentStack(upIntent)
                          // Navigate up to the closest parent
                          .startActivities();
              } else {
                  // This activity is part of this app's task, so simply
                  // navigate up to the logical parent activity.
                  NavUtils.navigateUpTo(this, upIntent);
              }
              return true;
          }
          return super.onOptionsItemSelected(item);
      }
  • Providing Proper Back Navigation

    • 安卓系统都有一个物理返回键,所以不应该在UI上额外添加一个返回键
    • 安卓系统的back stack通常情况下都可以应对back navigation
    • 但是以下情况需要特殊考虑

      • 从通知栏消息、widget、navigation drawer直接进入一个深层次的activity
          // Intent for the activity to open when user selects the notification
          Intent detailsIntent = new Intent(this, DetailsActivity.class);
      
          // Use TaskStackBuilder to build the back stack and get the PendingIntent
          PendingIntent pendingIntent =
                  TaskStackBuilder.create(this)
                                  // add all of DetailsActivity's parents to the stack,
                                  // followed by DetailsActivity itself
                                  .addNextIntentWithParentStack(upIntent)
                                  .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
      
          NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
          builder.setContentIntent(pendingIntent);
      • fragment之间的导航
          // Works with either the framework FragmentManager or the
          // support package FragmentManager (getSupportFragmentManager).
          getSupportFragmentManager().beginTransaction()
                                  .add(detailFragment, "detail")
                                  // Add this transaction to the back stack
                                  .addToBackStack()
                                  .commit();

      需要注意的是,当fragment是在ViewPager中水平切换时,不应该把transaction 加入到 backstack中。

      • WebView
          @Override
          public void onBackPressed() {
              if (mWebView.canGoBack()) {
                  mWebView.goBack();
                  return;
              }
      
              // Otherwise defer to system default behavior.
              super.onBackPressed();
          }
  • Implementing Descendant Navigation

    • 通常都是Intent加上startActivity(intent),或者FragmentTransaction来完成向子界面的导航
    • 需要注意的是,如果app将打开其他app的界面,为了防止用户中途离开,再次从launcher打开app时,却是其他app的界面,可以为intent设置FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET flag
      Intent externalActivityIntent = new Intent(Intent.ACTION_PICK);
      externalActivityIntent.setType("image/*");
      externalActivityIntent.addFlags(
              Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
      startActivity(externalActivityIntent);
  • Notifying the User

    • Building a Notification
      • 使用NotificationCompat.Builder创建通知栏消息
      • 响应用户对通知栏消息的点击,启动相应Activity,同时要考虑是否提供返回的导航设计
    • Preserving Navigation when Starting an Activity
      • 点击通知栏消息,可能会启动两种类型的Activity:正常使用流程会启动的Activity;仅仅是把通知栏消息展开的Activity;
      • 前者通常需要提供back导航支持,在manifest中声明Activity父子关系,同时构造PendingIntent时构造back stack,使用stackBuilder.getPendingIntent创建PendingIntent
      • 后者通常不需要加入到back stack中,无需手动构造back stack,使用PendingIntent.getActivity创建PendingIntent,同时在manifest中设置以下选项:
          <activity
              android:name=".ResultActivity"
          ...
              android:launchMode="singleTask"
              android:taskAffinity=""
              android:excludeFromRecents="true">
          </activity>
    • 完整示例:

      Intent resultIntent = new Intent(this, ResultActivity.class);
      TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
      // Adds the back stack
      stackBuilder.addParentStack(ResultActivity.class);
      // Adds the Intent to the top of the stack
      stackBuilder.addNextIntent(resultIntent);
      // Gets a PendingIntent containing the entire back stack
      PendingIntent resultPendingIntent =
              stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
      
      NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this)
          .setSmallIcon(R.drawable.notification_icon)
          .setContentTitle("My notification")
          .setContentText("Hello World!")
          .setContentIntent(resultPendingIntent);
      
      // Sets an ID for the notification
      int mNotificationId = 001;
      // Gets an instance of the NotificationManager service
      NotificationManager mNotifyMgr = 
              (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      // Builds the notification and issues it.
      mNotifyMgr.notify(mNotificationId, mBuilder.build());
    • Updating Notifications
      • 创建通知栏消息时,如果指定了id,以后可以通过id对已经显示的消息进行修改/更新、移除操作;
      • 修改/更新只需以相同的id,新的Notification对象,调用mNotifyMgr.notify即可;
      • 如果消息支持被移除,则用户可以手动移除(单个或者所有),setAutoCancel()会让消息在用户点击时自动消失;cancel(id)cancelAll()可以代码移除通知栏消息;
    • Using Big View Styles

      • Android 4.1之后,通知栏消息引入了action的支持,方便用户快捷操作
      // Sets up the Snooze and Dismiss action buttons that will appear in the
      // big view of the notification.
      Intent dismissIntent = new Intent(this, PingService.class);
      dismissIntent.setAction(CommonConstants.ACTION_DISMISS);
      PendingIntent piDismiss = PendingIntent.getService(this, 0, dismissIntent, 0);
      
      Intent snoozeIntent = new Intent(this, PingService.class);
      snoozeIntent.setAction(CommonConstants.ACTION_SNOOZE);
      PendingIntent piSnooze = PendingIntent.getService(this, 0, snoozeIntent, 0);
      
      // Constructs the Builder object.
      NotificationCompat.Builder builder =
              new NotificationCompat.Builder(this)
              .setSmallIcon(R.drawable.ic_stat_notification)
              .setContentTitle(getString(R.string.notification))
              .setContentText(getString(R.string.ping))
              .setDefaults(Notification.DEFAULT_ALL) // requires VIBRATE permission
              /*
              * Sets the big view "big text" style and supplies the
              * text (the user's reminder message) that will be displayed
              * in the detail area of the expanded notification.
              * These calls are ignored by the support library for
              * pre-4.1 devices.
              */
              .setStyle(new NotificationCompat.BigTextStyle()
                      .bigText(msg))
              .addAction (R.drawable.ic_stat_dismiss,
                      getString(R.string.dismiss), piDismiss)
              .addAction (R.drawable.ic_stat_snooze,
                      getString(R.string.snooze), piSnooze);
    • Displaying Progress in a Notification
      • setProgress (int max, int progress, boolean indeterminate)可以为通知栏消息设置进度条,支持实时进度、持续模式
  • Supporting Swipe-to-Refresh

  • Adding Search Functionality

    • 在App Bar中添加SearchView,注意它有support版本

      res/menu/options_menu.xml:

          <?xml version="1.0" encoding="utf-8"?>
          <menu xmlns:android="http://schemas.android.com/apk/res/android">
              <item android:id="@+id/search"
                  android:title="@string/search_title"
                  android:icon="@drawable/ic_search"
                  android:showAsAction="collapseActionView|ifRoom"
                  android:actionViewClass="android.widget.SearchView" />
          </menu>
          @Override
          public boolean onCreateOptionsMenu(Menu menu) {
              MenuInflater inflater = getMenuInflater();
              inflater.inflate(R.menu.options_menu, menu);
      
              return true;
          }
    • 创建可搜索的配置

      res/xml/searchable.xml,配置SearchView的label,hint

          <?xml version="1.0" encoding="utf-8"?>
          <searchable xmlns:android="http://schemas.android.com/apk/res/android"
                  android:label="@string/app_name"
                  android:hint="@string/search_hint" />

      manifest

          <activity ... >
              ...
              <meta-data android:name="android.app.searchable"
                      android:resource="@xml/searchable" />
      
          </activity>
          @Override
          public boolean onCreateOptionsMenu(Menu menu) {
              MenuInflater inflater = getMenuInflater();
              inflater.inflate(R.menu.options_menu, menu);
      
              // Associate searchable configuration with the SearchView
              SearchManager searchManager =
                  (SearchManager) getSystemService(Context.SEARCH_SERVICE);
              SearchView searchView =
                      (SearchView) menu.findItem(R.id.search).getActionView();
              searchView.setSearchableInfo(
                      searchManager.getSearchableInfo(getComponentName()));
      
              return true;
          }
    • 当用户提交一个query时,SearchView会发出一个ACTION_SEARCH intent,需要在manifest文件中声明响应此intent
  • Making Your App Content Searchable by Google

    • Google对如何优化对app内的内容、网站的搜索,提供了相应的建议
    • Deep Links,通过在manifest中声明感兴趣的Intent,可以在用户在其他app内触发此intent时启动自己的app,intent可以设置action, category, scheme来进行过滤
  • 优化app的assistant内容

  • app link,让自己的app成为自己网站uri的默认打开方式