向初次使用的用户介绍您的应用

如需向初次使用的用户说明如何充分利用您的应用,可以在应用启动时显示新手入门信息。以下是新手入门信息的一些示例:

  • 当用户第一次访问频道应用时,展示有关提供哪些频道的详细信息。
  • 提醒用户关注您的应用中值得注意的功能。
  • 说明用户在第一次使用应用时应当执行的必要步骤或建议步骤。

Leanback androidx 库提供了用于向初次使用的用户展示新手入门信息的 OnboardingSupportFragment 类。本节课介绍如何使用 OnboardingSupportFragment 类来展示应用首次启动时所显示的入门信息。OnboardingSupportFragment 利用 TV 界面最佳做法来展示信息,这种方式不仅适合 TV 界面风格,也方便在 TV 设备上导航。

图 1. OnboardingSupportFragment 示例。

您的 OnboardingSupportFragment 不应包含需要用户输入的界面元素,如按钮和字段。同样,也不应将其用作用户需要定期执行的任务的界面元素。

添加 OnboardingSupportFragment

如需在应用中添加 OnboardingSupportFragment,请实现一个用于扩展 OnboardingSupportFragment 类的类。将此 Fragment 通过 Activity 的布局 XML 或以编程方式添加到 Activity 中,同时确保 Activity 或 Fragment 采用派生自 Theme_Leanback_Onboarding 的主题背景。

在应用的主 Activity 的 onCreate() 方法中,通过指向 OnboardingSupportFragment's 父 Activity 的 Intent 调用 startActivity()。这可确保您的 OnboardingSupportFragment 在应用启动后立即显示。

若要确保 OnboardingSupportFragment] 仅在用户首次启动应用时显示,请使用 SharedPreferences 对象来跟踪用户是否已查看过 OnboardingSupportFragment。定义一个布尔值,使其在用户查看过 OnboardingSupportFragment 时变为 true。在主 Activity 的 onCreate() 中检查这个值,并且仅在值为 false 时才启动 OnboardingSupportFragment 父 Activity。以下示例展示了如何替换用于检查 SharedPreferences 值的 onCreate(),如果没有设为 true,则调用 startActivity() 来显示 OnboardingSupportFragment

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(this);
        // Check if we need to display our OnboardingSupportFragment
        if (!sharedPreferences.getBoolean(
                MyOnboardingSupportFragment.COMPLETED_ONBOARDING_PREF_NAME, false)) {
            // The user hasn't seen the OnboardingSupportFragment yet, so show it
            startActivity(new Intent(this, OnboardingActivity.class));
        }
    }

在用户查看 OnboardingSupportFragment 后,使用 SharedPreferences 对象将其标记为已查看。为此,请在您的 OnboardingSupportFragment 中替换 onFinishFragment() 并将 SharedPreferences 值设为 true,如以下示例所示:

    @Override
    protected void onFinishFragment() {
        super.onFinishFragment();
        // User has seen OnboardingSupportFragment, so mark our SharedPreferences
        // flag as completed so that we don't show our OnboardingSupportFragment
        // the next time the user launches the app.
        SharedPreferences.Editor sharedPreferencesEditor =
                PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
        sharedPreferencesEditor.putBoolean(
                COMPLETED_ONBOARDING_PREF_NAME, true);
        sharedPreferencesEditor.apply();
    }

注意:当切换到最后一页的时候,会显示"开始使用"的按钮,点击该按钮会调用onFinishFragment方法,因此可以在该方法中保存标记并调用Activity的finish方法关闭该引导页。

添加 OnboardingSupportFragment 页面

在添加 OnboardingSupportFragment 之后,您需要定义新手入门信息页。OnboardingSupportFragment` 可在一系列有序的页面中显示内容。每个页面可以包含标题、说明和若干包含图片或动画的子视图。

图 2. OnboardingSupportFragment 页面元素。

图 2 展示了一个示例页面,其中用标注标出了您的 OnboardingSupportFragment 可以提供的可自定义页面元素。页面元素包括:

  1. 页面标题。
  2. 页面说明。
  3. 页面内容视图,本例中是一个简单的绿色对勾显示在灰色方框中。此视图是可选的。此视图可用于展示页面详细信息,例如突出显示页面所介绍的应用功能的屏幕截图。
  4. 页面背景视图,本例中为一个简单的蓝色渐变背景。此视图始终呈现在页面上其他视图的后面。此视图是可选的。
  5. 页面前景视图,本例中为一个徽标。此视图始终呈现在页面上所有其他视图的前面。此视图是可选的。

替换以下各种向系统提供页面信息的方法:

  • getPageCount() 会返回 OnboardingSupportFragment 中的页数。
  • getPageTitle() 会返回所请求页码的标题。
  • getPageDescription() 会返回所请求页码的说明。

替换以下各种方法,以提供用于显示图片或动画的可选子视图:

  • onCreateBackgroundView() 会返回您创建用来作为背景视图的 View;如果不需要背景视图,则返回 null。
  • onCreateContentView() 会返回您创建用来作为内容视图的 View;如果不需要内容视图,则返回 null。
  • onCreateForegroundView() 会返回您创建用来作为前台视图的 View;如果不需要前台视图,则返回 null。

系统会将您创建的 View 添加到页面布局中。以下示例会替换 onCreateContentView() 并返回 ImageView

    private ImageView contentView;
    ...
    @Override
    protected View onCreateContentView(LayoutInflater inflater, ViewGroup container) {
        contentView = new ImageView(getContext());
        contentView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
        contentView.setImageResource(R.drawable.onboarding_content_view);
        contentView.setPadding(0, 32, 0, 32);
        return contentView;
    }

添加初始徽标屏幕

您的 OnboardingSupportFragment 可在启动时显示可选的徽标屏幕,用来介绍您的应用。如果您希望将 Drawable 显示为徽标屏幕,请在 OnboardingSupportFragment's onCreate() 方法中使用 Drawable 的 ID 调用 setLogoResourceId()。系统将淡入并短暂显示此 Drawable,再淡出 Drawable,然后显示 OnboardingSupportFragment 的第一个页面。

若要为徽标屏幕提供自定义动画,则不应调用 setLogoResourceId(),而应替换 onCreateLogoAnimation() 并返回一个渲染自定义动画的 Animator 对象,如以下示例所示:

    @Override
    public Animator onCreateLogoAnimation() {
        return AnimatorInflater.loadAnimator(getContext(),
                R.animator.onboarding_logo_screen_animation);
    }

自定义页面动画

在显示 OnboardingSupportFragment 的第一个页面时,以及当用户导航到另一个页面时,系统会使用默认动画。您可以通过替换 OnboardingSupportFragment 中的方法来自定义这些动画。

若要自定义在第一个页面上显示的动画,请替换 onCreateEnterAnimation() 并返回 Animator。以下示例会创建一个 Animator,它会在水平方向上缩放内容视图:

    @Override
    protected Animator onCreateEnterAnimation() {
        Animator startAnimator = ObjectAnimator.ofFloat(contentView,
                View.SCALE_X, 0.2f, 1.0f).setDuration(ANIMATION_DURATION);
        return startAnimator;
    }

若要自定义用户导航到另一个页面时使用的动画,请替换 onPageChanged()。在 onPageChanged() 方法中,创建用于移除上一页并显示下一页的Animators,将它们添加到AnimatorSet` 中,再播放这个集合。以下示例使用一个淡出动画来移除上一页,更新内容视图图像,然后使用淡入动画来显示下一页:

    @Override
    protected void onPageChanged(final int newPage, int previousPage) {
        // Create a fade-out animation used to fade out previousPage and, once
        // done, swaps the contentView image with the next page's image.
        Animator fadeOut = ObjectAnimator.ofFloat(mContentView,
                View.ALPHA, 1.0f, 0.0f).setDuration(ANIMATION_DURATION);
        fadeOut.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mContentView.setImageResource(pageImages[newPage]);
            }
        });
        // Create a fade-in animation used to fade in nextPage
        Animator fadeIn = ObjectAnimator.ofFloat(mContentView,
                View.ALPHA, 0.0f, 1.0f).setDuration(ANIMATION_DURATION);
        // Create AnimatorSet with our fade-out and fade-in animators, and start it
        AnimatorSet set = new AnimatorSet();
        set.playSequentially(fadeOut, fadeIn);
        set.start();
    }

自定义主题背景

任何 OnboardingSupportFragment 实现都必须使用 Theme_Leanback_Onboarding 主题背景,或继承自 Theme_Leanback_Onboarding 的主题背景。若要为您的 OnboardingSupportFragment 设置主题背景,请执行以下某项操作:

  • 设置 OnboardingSupportFragment's 父 Activity 以使用所需的主题背景。以下示例展示了如何设置 Activity 以使用应用清单中的 Theme_Leanback_Onboarding

     <activity
           android:name=".OnboardingActivity"
           android:enabled="true"
           android:exported="true"
           android:theme="@style/Theme.Leanback.Onboarding">
     </activity>
    
  • 通过在自定义 Activity 主题背景中使用 LeanbackOnboardingTheme_onboardingTheme 属性,设置父 Activity 中的主题背景。将该属性指向只有您的 Activity 中的 OnboardingSupportFragment 对象会使用的另一个自定义主题背景。如果您的 Activity 已使用了一个自定义主题背景,并且您不想将 OnboardingSupportFragment 样式应用到 Activity 中的其他视图,请采用这种方法。

  • 替换 onProvideTheme() 并返回所需的主题背景。如果有多个 Activity 使用您的 OnboardingSupportFragment,或者父 Activity 无法使用所需的主题背景,请采用这种方法。以下示例会替换 onProvideTheme() 并返回 Theme_Leanback_Onboarding

    @Override
    public int onProvideTheme() {
       return R.style.Theme_Leanback_Onboarding;
    }

至此,关于如何"向初次使用的用户介绍您的应用"就介绍完了,但是我相信小伙伴们还是一头雾水的吧,因为理论太多,而是代码过于碎片化。

实战

添加Leanback库

dependencies {
    ...
    implementation 'androidx.leanback:leanback:1.0.0'
}

启动页(MainActivity)

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(this);
        // Check if we need to display our OnboardingSupportFragment
        if (!sharedPreferences.getBoolean(
                BoardingSupportFragment.COMPLETED_ONBOARDING_PREF_NAME, false)) {
            // The user hasn't seen the OnboardingSupportFragment yet, so show it
            startActivity(new Intent(this, OnboardingActivity.class));
        }
    }
}

添加OnboardingActivity

添加OnboardingActivity继承FragmentActivity

public class OnboardingActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_onboarding);
    }
}

activity_onboarding.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".OnboardingActivity">

    <fragment
        android:id="@+id/BoardingSupportFragment"
        android:name="com.winsat.myapplication.BoardingSupportFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

BoardingSupportFragment实现

public class BoardingSupportFragment extends OnboardingSupportFragment {

    public static final String COMPLETED_ONBOARDING_PREF_NAME = "COMPLETED_ONBOARDING_PREF_NAME";

    private String[] titles = {"Page1", "Page2", "Page3"};
    private String[] descs = {"This is page1", "This is page2", "This is page3"};
    private int[] contentImags = {R.drawable.page1, R.drawable.page2, R.drawable.page3};
    ImageView contentView;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //添加初始徽标屏幕,会在显示第一页之前显示ic_launcher
        setLogoResourceId(R.mipmap.ic_launcher);
    }

    @Override
    public int onProvideTheme() {
        //这里设置主题,或者直接设置该Fragment对应的Activity的主题为R.style.Theme_Leanback_Onboarding;
        return R.style.Theme_Leanback_Onboarding;
    }

    @Nullable
    @Override
    protected Animator onCreateEnterAnimation() {
        //第一个页面上显示的动画
        return ObjectAnimator.ofFloat(contentView,
                View.SCALE_X, 0.2f, 1.0f).setDuration(ANIMATION_DURATION);
    }

    @Override
    protected int getPageCount() {
        return titles.length;
    }

    @Override
    protected CharSequence getPageTitle(int pageIndex) {
        return titles[pageIndex];
    }

    @Override
    protected CharSequence getPageDescription(int pageIndex) {
        return descs[pageIndex];
    }

    @Nullable
    @Override
    protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container) {
        //背景View是一个全屏的View
        return null;
    }

    @Nullable
    @Override
    protected View onCreateContentView(LayoutInflater inflater, ViewGroup container) {
        //这里需要将contentView作为全局变量,onPageChanged方法中更新图片,背景和前景图片类似
        contentView = new ImageView(getContext());
        contentView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
        int pageIndex = getCurrentPageIndex();
        contentView.setImageResource(contentImags[pageIndex]);
        contentView.setPadding(0, 32, 0, 32);
        return contentView;
    }

    @Override
    protected void onPageChanged(final int newPage, int previousPage) {
        super.onPageChanged(newPage, previousPage);
//        //当界面发生变化时,contentView设置对应位置的图片,背景和前景图片类似
//        contentView.setImageResource(contentImags[newPage]);

        // Create a fade-out animation used to fade out previousPage and, once
        // done, swaps the contentView image with the next page's image.
        Animator fadeOut = ObjectAnimator.ofFloat(contentView,
                View.ALPHA, 1.0f, 0.0f).setDuration(ANIMATION_DURATION);
        fadeOut.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //当界面发生变化时,contentView设置对应位置的图片,背景和前景图片类似
                contentView.setImageResource(contentImags[newPage]);
            }
        });
        // Create a fade-in animation used to fade in nextPage
        Animator fadeIn = ObjectAnimator.ofFloat(contentView,
                View.ALPHA, 0.0f, 1.0f).setDuration(ANIMATION_DURATION);
        // Create AnimatorSet with our fade-out and fade-in animators, and start it
        AnimatorSet set = new AnimatorSet();
        set.playSequentially(fadeOut, fadeIn);
        set.start();

    }

    @Nullable
    @Override
    protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container) {
        //前景View是一个全屏的View,因此添加View的时候需要考虑到子View对应的位置
        return null;
    }

    @Override
    protected void onFinishFragment() {
        super.onFinishFragment();
        // User has seen OnboardingSupportFragment, so mark our SharedPreferences
        // flag as completed so that we don't show our OnboardingSupportFragment
        // the next time the user launches the app.
        SharedPreferences.Editor sharedPreferencesEditor =
                PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
        sharedPreferencesEditor.putBoolean(
                COMPLETED_ONBOARDING_PREF_NAME, true);
        sharedPreferencesEditor.apply();
        FragmentActivity fragmentActivity = getActivity();
        if (fragmentActivity != null) {
            fragmentActivity.finish();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341