Add titles to PreferenceFragments and PreferenceScreens for watches.

Preferences lack a title on watch type devices due to lack of ActionBar
support. A custom ListView was added to use a custom wrapper adapter to
add a persistent header view at the top of the ListView that developers
would not be able to remove via the ListView API.

Bug: 27962897
Change-Id: I6bccecf85592d9507e0c7a04c9a035617001e9ef
This commit is contained in:
Michael Kwan
2016-07-22 18:37:31 -07:00
parent ad74941699
commit 744be16ffa
8 changed files with 408 additions and 13 deletions

View File

@@ -27,12 +27,14 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
/**
* Shows a hierarchy of {@link Preference} objects as
@@ -366,6 +368,20 @@ public abstract class PreferenceFragment extends Fragment implements
private void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
View root = getView();
if (root != null) {
View titleView = root.findViewById(android.R.id.title);
if (titleView instanceof TextView) {
CharSequence title = preferenceScreen.getTitle();
if (TextUtils.isEmpty(title)) {
titleView.setVisibility(View.GONE);
} else {
((TextView) titleView).setText(title);
titleView.setVisibility(View.VISIBLE);
}
}
}
preferenceScreen.bind(getListView());
}
onBindPreferences();

View File

@@ -19,6 +19,8 @@ package android.preference;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -31,6 +33,7 @@ import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
/**
* Represents a top-level {@link Preference} that
@@ -91,13 +94,33 @@ public final class PreferenceScreen extends PreferenceGroup implements AdapterVi
private Dialog mDialog;
private ListView mListView;
private int mLayoutResId = com.android.internal.R.layout.preference_list_fragment;
private Drawable mDividerDrawable;
private boolean mDividerSpecified;
/**
* Do NOT use this constructor, use {@link PreferenceManager#createPreferenceScreen(Context)}.
* @hide-
*/
public PreferenceScreen(Context context, AttributeSet attrs) {
super(context, attrs, com.android.internal.R.attr.preferenceScreenStyle);
TypedArray a = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.PreferenceScreen,
com.android.internal.R.attr.preferenceScreenStyle,
0);
mLayoutResId = a.getResourceId(
com.android.internal.R.styleable.PreferenceScreen_screenLayout,
mLayoutResId);
if (a.hasValueOrEmpty(com.android.internal.R.styleable.PreferenceScreen_divider)) {
mDividerDrawable =
a.getDrawable(com.android.internal.R.styleable.PreferenceScreen_divider);
mDividerSpecified = true;
}
a.recycle();
}
/**
@@ -163,18 +186,30 @@ public final class PreferenceScreen extends PreferenceGroup implements AdapterVi
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View childPrefScreen = inflater.inflate(
com.android.internal.R.layout.preference_list_fragment, null);
View childPrefScreen = inflater.inflate(mLayoutResId, null);
View titleView = childPrefScreen.findViewById(android.R.id.title);
mListView = (ListView) childPrefScreen.findViewById(android.R.id.list);
if (mDividerSpecified) {
mListView.setDivider(mDividerDrawable);
}
bind(mListView);
// Set the title bar if title is available, else no title bar
final CharSequence title = getTitle();
Dialog dialog = mDialog = new Dialog(context, context.getThemeResId());
if (TextUtils.isEmpty(title)) {
if (titleView != null) {
titleView.setVisibility(View.GONE);
}
dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
} else {
dialog.setTitle(title);
if (titleView instanceof TextView) {
((TextView) titleView).setText(title);
titleView.setVisibility(View.VISIBLE);
} else {
dialog.setTitle(title);
}
}
dialog.setContentView(childPrefScreen);
dialog.setOnDismissListener(this);

View File

@@ -112,8 +112,8 @@ public class ListView extends AbsListView {
public boolean isSelectable;
}
private ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
private ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
Drawable mDivider;
int mDividerHeight;
@@ -279,7 +279,7 @@ public class ListView extends AbsListView {
// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
wrapHeaderListAdapterInternal();
}
// In the case of re-adding a header view, or adding one later on,
@@ -373,7 +373,7 @@ public class ListView extends AbsListView {
// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
wrapHeaderListAdapterInternal();
}
// In the case of re-adding a footer view, or adding one later on,
@@ -476,7 +476,7 @@ public class ListView extends AbsListView {
mRecycler.clear();
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
@@ -2228,7 +2228,7 @@ public class ListView extends AbsListView {
* after the header views.
*/
public void setSelectionAfterHeaderView() {
final int count = mHeaderViewInfos.size();
final int count = getHeaderViewsCount();
if (count > 0) {
mNextSelectedPosition = 0;
return;
@@ -3356,7 +3356,7 @@ public class ListView extends AbsListView {
bounds.right = mRight - mLeft - mPaddingRight;
final int count = getChildCount();
final int headerCount = mHeaderViewInfos.size();
final int headerCount = getHeaderViewsCount();
final int itemCount = mItemCount;
final int footerLimit = (itemCount - mFooterViewInfos.size());
final boolean headerDividers = mHeaderDividersEnabled;
@@ -3940,7 +3940,7 @@ public class ListView extends AbsListView {
if (drawDividers) {
final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
final int itemCount = mItemCount;
final int headerCount = mHeaderViewInfos.size();
final int headerCount = getHeaderViewsCount();
final int footerLimit = (itemCount - mFooterViewInfos.size());
final boolean isHeader = (itemIndex < headerCount);
final boolean isFooter = (itemIndex >= footerLimit);
@@ -4052,4 +4052,24 @@ public class ListView extends AbsListView {
encoder.addProperty("recycleOnMeasure", recycleOnMeasure());
}
/** @hide */
protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
ArrayList<ListView.FixedViewInfo> headerViewInfos,
ArrayList<ListView.FixedViewInfo> footerViewInfos,
ListAdapter adapter) {
return new HeaderViewListAdapter(headerViewInfos, footerViewInfos, adapter);
}
/** @hide */
protected void wrapHeaderListAdapterInternal() {
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
/** @hide */
protected void dispatchDataSetObserverOnChangedInternal() {
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2016 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.internal.widget;
import android.annotation.IdRes;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.HeaderViewListAdapter;
import java.util.ArrayList;
import com.android.internal.util.Predicate;
public class WatchHeaderListView extends ListView {
private View mTopPanel;
public WatchHeaderListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public WatchHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public WatchHeaderListView(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
ArrayList<ListView.FixedViewInfo> headerViewInfos,
ArrayList<ListView.FixedViewInfo> footerViewInfos,
ListAdapter adapter) {
return new WatchHeaderListAdapter(headerViewInfos, footerViewInfos, adapter);
}
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
if (mTopPanel == null) {
setTopPanel(child);
} else {
throw new IllegalStateException("WatchHeaderListView can host only one header");
}
}
public void setTopPanel(View v) {
mTopPanel = v;
wrapAdapterIfNecessary();
}
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
wrapAdapterIfNecessary();
}
@Override
protected View findViewTraversal(@IdRes int id) {
View v = super.findViewTraversal(id);
if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
return mTopPanel.findViewById(id);
}
return v;
}
@Override
protected View findViewWithTagTraversal(Object tag) {
View v = super.findViewWithTagTraversal(tag);
if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
return mTopPanel.findViewWithTag(tag);
}
return v;
}
@Override
protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) {
View v = super.findViewByPredicateTraversal(predicate, childToSkip);
if (v == null && mTopPanel != null && mTopPanel != childToSkip
&& !mTopPanel.isRootNamespace()) {
return mTopPanel.findViewByPredicate(predicate);
}
return v;
}
@Override
public int getHeaderViewsCount() {
return mTopPanel == null ? super.getHeaderViewsCount() : super.getHeaderViewsCount() + 1;
}
private void wrapAdapterIfNecessary() {
ListAdapter adapter = getAdapter();
if (adapter != null && mTopPanel != null) {
if (!(adapter instanceof WatchHeaderListAdapter)) {
wrapHeaderListAdapterInternal();
}
((WatchHeaderListAdapter) getAdapter()).setTopPanel(mTopPanel);
dispatchDataSetObserverOnChangedInternal();
}
}
private static class WatchHeaderListAdapter extends HeaderViewListAdapter {
private View mTopPanel;
public WatchHeaderListAdapter(
ArrayList<ListView.FixedViewInfo> headerViewInfos,
ArrayList<ListView.FixedViewInfo> footerViewInfos,
ListAdapter adapter) {
super(headerViewInfos, footerViewInfos, adapter);
}
public void setTopPanel(View v) {
mTopPanel = v;
}
private int getTopPanelCount() {
return mTopPanel == null ? 0 : 1;
}
@Override
public int getCount() {
return super.getCount() + getTopPanelCount();
}
@Override
public boolean areAllItemsEnabled() {
return mTopPanel == null && super.areAllItemsEnabled();
}
@Override
public boolean isEnabled(int position) {
if (mTopPanel != null) {
if (position == 0) {
return false;
} else {
return super.isEnabled(position - 1);
}
}
return super.isEnabled(position);
}
@Override
public Object getItem(int position) {
if (mTopPanel != null) {
if (position == 0) {
return null;
} else {
return super.getItem(position - 1);
}
}
return super.getItem(position);
}
@Override
public long getItemId(int position) {
int numHeaders = getHeadersCount() + getTopPanelCount();
if (getWrappedAdapter() != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = getWrappedAdapter().getCount();
if (adjPosition < adapterCount) {
return getWrappedAdapter().getItemId(adjPosition);
}
}
return -1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (mTopPanel != null) {
if (position == 0) {
return mTopPanel;
} else {
return super.getView(position - 1, convertView, parent);
}
}
return super.getView(position, convertView, parent);
}
@Override
public int getItemViewType(int position) {
int numHeaders = getHeadersCount() + getTopPanelCount();
if (getWrappedAdapter() != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = getWrappedAdapter().getCount();
if (adjPosition < adapterCount) {
return getWrappedAdapter().getItemViewType(adjPosition);
}
}
return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
}
}
}

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 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:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@android:color/transparent"
android:layout_removeBorders="true">
<FrameLayout
android:id="@android:id/list_container"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1">
<com.android.internal.widget.WatchHeaderListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="?attr/preferenceFragmentListStyle"
android:scrollbarStyle="@integer/preference_fragment_scrollbarStyle"
android:clipToPadding="false"
android:drawSelectorOnTop="false"
android:cacheColorHint="@android:color/transparent"
android:scrollbarAlwaysDrawVerticalTrack="true">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/dialog_padding_material"
android:paddingEnd="@dimen/dialog_padding_material"
android:paddingBottom="8dp"
android:textAppearance="@style/TextAppearance.Material.Title"
android:gravity="center" />
</com.android.internal.widget.WatchHeaderListView>
</FrameLayout>
<TextView android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/preference_fragment_padding_side"
android:gravity="center"
android:visibility="gone" />
<RelativeLayout android:id="@+id/button_bar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="0"
android:visibility="gone">
<Button android:id="@+id/back_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:layout_alignParentStart="true"
android:text="@string/back_button_label"
/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true">
<Button android:id="@+id/skip_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:text="@string/skip_button_label"
android:visibility="gone"
/>
<Button android:id="@+id/next_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:text="@string/next_button_label"
/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@@ -57,6 +57,10 @@ please see styles_device_defaults.xml.
<item name="divider">@empty</item>
</style>
<style name="Preference.Material.PreferenceScreen" parent="Preference.Material.BasePreferenceScreen">
<item name="divider">@empty</item>
</style>
<style name="Widget.Material.TextView" parent="Widget.TextView">
<item name="breakStrategy">balanced</item>
</style>

View File

@@ -7901,6 +7901,13 @@ i
<attr name="divider" />
</declare-styleable>
<!-- Base attributes available to PreferenceScreen. -->
<declare-styleable name="PreferenceScreen">
<!-- The layout for the PreferenceScreen. This should rarely need to be changed. -->
<attr name="screenLayout" format="reference" />
<attr name="divider" />
</declare-styleable>
<!-- Base attributes available to PreferenceActivity. -->
<declare-styleable name="PreferenceActivity">
<!-- The layout for the Preference Activity. This should rarely need to be changed. -->

View File

@@ -81,7 +81,12 @@ please see styles_device_defaults.xml.
<item name="layout">@layout/preference_widget_seekbar_material</item>
</style>
<style name="Preference.Material.PreferenceScreen"/>
<style name="Preference.Material.BasePreferenceScreen">
<item name="screenLayout">@layout/preference_list_fragment_material</item>
<item name="divider">?attr/listDivider</item>
</style>
<style name="Preference.Material.PreferenceScreen" parent="Preference.Material.BasePreferenceScreen"/>
<style name="Preference.Material.DialogPreference">
<item name="positiveButtonText">@string/ok</item>