Create a banner message widget

Based on our ux spec, we create this widget to
let everyone can follow up our spec easily.

Bug: 173184155
Test: Run robotest and apply this widget in Settings and see the ui
Change-Id: I65ebf5f5d9c59e9e52bfb5ad0d8a003a9642303a
This commit is contained in:
Tsung-Mao Fang
2020-11-13 18:34:02 +08:00
parent a74ecc3709
commit 0c4187fda7
8 changed files with 498 additions and 0 deletions

View File

@@ -53,6 +53,7 @@ java_defaults {
"SettingsLibUtils",
"SettingsLibEmergencyNumber",
"SettingsLibTopIntroPreference",
"SettingsLibBannerMessagePreference",
],
}

View File

@@ -0,0 +1,13 @@
android_library {
name: "SettingsLibBannerMessagePreference",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
static_libs: [
"androidx.preference_preference",
],
sdk_version: "system_current",
min_sdk_version: "21",
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.settingslib.widget">
<uses-sdk android:minSdkVersion="21"/>
</manifest>

View File

@@ -0,0 +1,26 @@
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M1,21L12,2L23,21H1ZM19.53,19L12,5.99L4.47,19H19.53ZM11,16V18H13V16H11ZM11,10H13V14H11V10Z"
android:fillColor="?android:attr/colorError"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="16dp"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_warning"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:orientation="vertical">
<TextView
android:id="@+id/banner_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/Banner.Text.Title"/>
<TextView
android:id="@+id/banner_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/Banner.Text.Summary"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/banner_negative_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.DeviceDefault.Button.Borderless.Colored"/>
<Button
android:id="@+id/banner_positive_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.DeviceDefault.Button.Borderless.Colored"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<style name="Banner.Text.Title"
parent="@android:style/TextAppearance.Material.Subhead">
<item name="android:textSize">16sp</item>
<item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="Banner.Text.Summary"
parent="@*android:style/TextAppearance.DeviceDefault.Body1">
<item name="android:textColor">?android:attr/textColorSecondary</item>
<item name="android:textSize">14sp</item>
</style>
</resources>

View File

@@ -0,0 +1,182 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.widget;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.StringRes;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
/**
* Banner message is a banner displaying important information (permission request, page error etc),
* and provide actions for user to address. It requires a user action to be dismissed.
*/
public class BannerMessagePreference extends Preference {
private static final String TAG = "BannerPreference";
private BannerMessagePreference.ButtonInfo mPositiveButtonInfo;
private BannerMessagePreference.ButtonInfo mNegativeButtonInfo;
public BannerMessagePreference(Context context) {
super(context);
init();
}
public BannerMessagePreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.setDividerAllowedAbove(true);
holder.setDividerAllowedBelow(true);
mPositiveButtonInfo.mButton = (Button) holder.findViewById(R.id.banner_positive_btn);
mNegativeButtonInfo.mButton = (Button) holder.findViewById(R.id.banner_negative_btn);
mPositiveButtonInfo.setUpButton();
mNegativeButtonInfo.setUpButton();
final TextView titleView = (TextView) holder.findViewById(R.id.banner_title);
final TextView summaryView = (TextView) holder.findViewById(R.id.banner_summary);
titleView.setText(getTitle());
summaryView.setText(getSummary());
}
private void init() {
mPositiveButtonInfo = new BannerMessagePreference.ButtonInfo();
mNegativeButtonInfo = new BannerMessagePreference.ButtonInfo();
setSelectable(false);
setLayoutResource(R.layout.banner_message);
}
/**
* Set the visibility state of positive button.
*/
public BannerMessagePreference setPositiveButtonVisible(boolean isVisible) {
if (isVisible != mPositiveButtonInfo.mIsVisible) {
mPositiveButtonInfo.mIsVisible = isVisible;
notifyChanged();
}
return this;
}
/**
* Set the visibility state of negative button.
*/
public BannerMessagePreference setNegativeButtonVisible(boolean isVisible) {
if (isVisible != mNegativeButtonInfo.mIsVisible) {
mNegativeButtonInfo.mIsVisible = isVisible;
notifyChanged();
}
return this;
}
/**
* Register a callback to be invoked when positive button is clicked.
*/
public BannerMessagePreference setPositiveButtonOnClickListener(
View.OnClickListener listener) {
if (listener != mPositiveButtonInfo.mListener) {
mPositiveButtonInfo.mListener = listener;
notifyChanged();
}
return this;
}
/**
* Register a callback to be invoked when negative button is clicked.
*/
public BannerMessagePreference setNegativeButtonOnClickListener(
View.OnClickListener listener) {
if (listener != mNegativeButtonInfo.mListener) {
mNegativeButtonInfo.mListener = listener;
notifyChanged();
}
return this;
}
/**
* Sets the text to be displayed in positive button.
*/
public BannerMessagePreference setPositiveButtonText(@StringRes int textResId) {
final String newText = getContext().getString(textResId);
if (!TextUtils.equals(newText, mPositiveButtonInfo.mText)) {
mPositiveButtonInfo.mText = newText;
notifyChanged();
}
return this;
}
/**
* Sets the text to be displayed in negative button.
*/
public BannerMessagePreference setNegativeButtonText(@StringRes int textResId) {
final String newText = getContext().getString(textResId);
if (!TextUtils.equals(newText, mNegativeButtonInfo.mText)) {
mNegativeButtonInfo.mText = newText;
notifyChanged();
}
return this;
}
static class ButtonInfo {
private Button mButton;
private CharSequence mText;
private View.OnClickListener mListener;
private boolean mIsVisible = true;
void setUpButton() {
mButton.setText(mText);
mButton.setOnClickListener(mListener);
if (shouldBeVisible()) {
mButton.setVisibility(View.VISIBLE);
} else {
mButton.setVisibility(View.GONE);
}
}
/**
* By default, two buttons are visible.
* If user didn't set a text for a button, then it should not be shown.
*/
private boolean shouldBeVisible() {
return mIsVisible && (!TextUtils.isEmpty(mText));
}
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.widget;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.preference.PreferenceViewHolder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public class BannerMessagePreferenceTest {
private Context mContext;
private View mRootView;
private BannerMessagePreference mBannerPreference;
private PreferenceViewHolder mHolder;
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
mRootView = View.inflate(mContext, R.layout.banner_message, null /* parent */);
mHolder = PreferenceViewHolder.createInstanceForTests(mRootView);
mBannerPreference = new BannerMessagePreference(mContext);
}
@Test
public void onBindViewHolder_shouldSetTitle() {
mBannerPreference.setTitle("test");
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((TextView) mRootView.findViewById(R.id.banner_title)).getText())
.isEqualTo("test");
}
@Test
public void onBindViewHolder_shouldSetSummary() {
mBannerPreference.setSummary("test");
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((TextView) mRootView.findViewById(R.id.banner_summary)).getText())
.isEqualTo("test");
}
@Test
public void setPositiveButtonText_shouldShowPositiveButton() {
mBannerPreference.setPositiveButtonText(R.string.tts_settings_title);
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_positive_btn)).getVisibility())
.isEqualTo(View.VISIBLE);
}
@Test
public void setNegativeButtonText_shouldShowNegativeButton() {
mBannerPreference.setNegativeButtonText(R.string.tts_settings_title);
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_negative_btn)).getVisibility())
.isEqualTo(View.VISIBLE);
}
@Test
public void withoutSetPositiveButtonText_shouldHidePositiveButton() {
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_positive_btn)).getVisibility())
.isEqualTo(View.GONE);
}
@Test
public void withoutSetNegativeButtonText_shouldHideNegativeButton() {
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_negative_btn)).getVisibility())
.isEqualTo(View.GONE);
}
@Test
public void setPositiveButtonVisible_withTrue_shouldShowPositiveButton() {
mBannerPreference.setPositiveButtonText(R.string.tts_settings_title);
mBannerPreference.setPositiveButtonVisible(true);
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_positive_btn)).getVisibility())
.isEqualTo(View.VISIBLE);
}
@Test
public void setPositiveButtonVisible_withFalse_shouldHidePositiveButton() {
mBannerPreference.setPositiveButtonText(R.string.tts_settings_title);
mBannerPreference.setPositiveButtonVisible(false);
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_positive_btn)).getVisibility())
.isEqualTo(View.GONE);
}
@Test
public void setNegativeButtonVisible_withTrue_shouldShowNegativeButton() {
mBannerPreference.setNegativeButtonText(R.string.tts_settings_title);
mBannerPreference.setNegativeButtonVisible(true);
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_negative_btn)).getVisibility())
.isEqualTo(View.VISIBLE);
}
@Test
public void setNegativeButtonVisible_withFalse_shouldHideNegativeButton() {
mBannerPreference.setNegativeButtonText(R.string.tts_settings_title);
mBannerPreference.setNegativeButtonVisible(false);
mBannerPreference.onBindViewHolder(mHolder);
assertThat(((Button) mRootView.findViewById(R.id.banner_negative_btn)).getVisibility())
.isEqualTo(View.GONE);
}
}