SearchFragment and SearchBar in support-leanback
Change-Id: Id958ad47462d9efd2b5b770c0391477190e0d66a
diff --git a/v17/leanback/res/layout/lb_search_bar.xml b/v17/leanback/res/layout/lb_search_bar.xml
new file mode 100644
index 0000000..55eeda3
--- /dev/null
+++ b/v17/leanback/res/layout/lb_search_bar.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+<android.support.v17.leanback.widget.SearchBar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lb_search_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <RelativeLayout
+ android:id="@+id/lb_search_bar_layout"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/lb_search_bar_height"
+ android:clipChildren="false"
+ android:layout_alignParentTop="true"
+ android:layout_gravity="top"
+ android:background="@color/lb_search_bar_background" >
+ <FrameLayout
+ android:id="@+id/lb_search_bar_items"
+ android:layout_width="@dimen/lb_search_bar_items_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"
+ android:layout_marginLeft="@dimen/lb_search_browse_row_padding_left"
+ android:layout_marginTop="@dimen/lb_search_bar_items_layout_margin_top"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:orientation="horizontal"
+ android:background="@android:color/transparent"
+ android:layout_weight="1">
+ <EditText
+ android:id="@+id/lb_search_text_editor"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:cursorVisible="true"
+ android:editable="true"
+ android:background="@null"
+ android:fontFamily="sans-serif-light"
+ android:imeOptions="normal|flagNoExtractUi"
+ android:inputType="text|textAutoComplete"
+ android:singleLine="true"
+ android:textColor="@color/lb_search_text_primary_color"
+ android:textCursorDrawable="@null"
+ android:textSize="34sp"/>
+ </FrameLayout>
+ </RelativeLayout>
+</android.support.v17.leanback.widget.SearchBar>
diff --git a/v17/leanback/res/layout/lb_search_fragment.xml b/v17/leanback/res/layout/lb_search_fragment.xml
new file mode 100644
index 0000000..0676628
--- /dev/null
+++ b/v17/leanback/res/layout/lb_search_fragment.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2014 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lb_search_dummy"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- The outer frame (search_dummy) exists to workaround the
+ problem where clients who embed us using a fragment tag in their
+ xml layout will override the view id. This causes problems managing
+ child fragments. Everything within the BrowseFrameLayout below
+ should not be affected by usage. -->
+ <RelativeLayout
+ android:id="@+id/lb_search_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <FrameLayout
+ android:id="@+id/lb_results_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_below="@id/lb_search_bar_layout"
+ android:layout_marginTop="@dimen/lb_search_bar_height"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:visibility="gone" >
+
+ <!-- Will be replaced by RowContainerFragment -->
+ <FrameLayout
+ android:id="@+id/lb_results_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/lb_search_fragment_results_container_height"
+ android:background="@android:color/transparent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:focusable="false"
+ android:visibility="visible" />
+
+ </FrameLayout>
+
+ <include
+ android:id="@+id/lb_search_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ layout="@layout/lb_search_bar" />
+ </RelativeLayout>
+</FrameLayout>
diff --git a/v17/leanback/res/values/colors.xml b/v17/leanback/res/values/colors.xml
index 2b1b9e3..a8ae64b 100644
--- a/v17/leanback/res/values/colors.xml
+++ b/v17/leanback/res/values/colors.xml
@@ -48,4 +48,8 @@
<color name="lb_view_dim_mask_color">#000000</color>
<item name="lb_view_dimmed_level" type="dimen">60%</item>
+ <color name="lb_search_background">#D0000000</color>
+ <color name="lb_search_bar_background">#992D2D2D</color>
+ <color name="lb_search_result_background">#FF2D2D2D</color>
+ <color name="lb_search_text_primary_color">#FFFCFCFC</color>
</resources>
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
index ec62496..cbeb129 100644
--- a/v17/leanback/res/values/dimens.xml
+++ b/v17/leanback/res/values/dimens.xml
@@ -14,9 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
+<resources>
<dimen name="lb_card_width">140dip</dimen>
<dimen name="lb_card_height">200dip</dimen>
<dimen name="lb_card_title_text_size">14sp</dimen>
@@ -92,4 +90,12 @@
<!-- One-sided increase in browse item size if height > width -->
<dimen name="lb_browse_item_zoom_width">12dp</dimen>
+ <!-- Search bar -->
+ <dimen name="lb_search_bar_height">120dip</dimen>
+ <dimen name="lb_search_bar_items_layout_margin_top">42dip</dimen>
+ <dimen name="lb_search_bar_items_width">660dip</dimen>
+
+ <!-- Search Fragment -->
+ <dimen name="lb_search_browse_row_padding_left">48dip</dimen>
+ <dimen name="lb_search_fragment_results_container_height">425dip</dimen>
</resources>
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
new file mode 100644
index 0000000..9932a9f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2014 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 android.support.v17.leanback.app;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemSelectedListener;
+import android.support.v17.leanback.widget.SearchBar;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+import android.support.v17.leanback.R;
+
+/**
+ * A fragment to handle searches
+ */
+public class SearchFragment extends Fragment {
+ private static final String TAG = SearchFragment.class.getSimpleName();
+ private static final String ARG_QUERY = SearchFragment.class.getCanonicalName() + ".query";
+
+ /**
+ * Search API exposed to application
+ */
+ public static interface SearchResultProvider {
+ /**
+ * When using the SearchFragment, this is the entry point for the application
+ * to receive the search query and provide the corresponding results.
+ *
+ * The returned ObjectAdapter can be populated asynchronously.
+ *
+ * As results are retrieved, the application should use the data set notification methods
+ * on the ObjectAdapter to instruct the SearchFragment to update the results.
+ *
+ * @param searchQuery The search query entered by the user.
+ * @return An ObjectAdapter containing the structured results for the provided search query.
+ */
+ public ObjectAdapter results(String searchQuery);
+ }
+
+ private final RowsFragment mRowsFragment = new RowsFragment();
+ private final Handler mHandler = new Handler();
+
+ private RelativeLayout mSearchFrame;
+ private SearchBar mSearchBar;
+ private FrameLayout mResultsFrame;
+ private SearchResultProvider mProvider;
+ private String mPendingQuery = null;
+
+ /**
+ * @param args Bundle to use for the arguments, if null a new Bundle will be created.
+ */
+ public static Bundle createArgs(Bundle args, String query) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putString(ARG_QUERY, query);
+ return args;
+ }
+
+ /**
+ * Create a search fragment with a given search query to start with
+ *
+ * You should only use this if you need to start the search fragment with a pre-filled query
+ *
+ * @param query the search query to start with
+ * @return a new SearchFragment
+ */
+ public static SearchFragment newInstance(String query) {
+ SearchFragment fragment = new SearchFragment();
+ Bundle args = createArgs(null, query);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
+
+ mSearchFrame = (RelativeLayout) root.findViewById(R.id.lb_search_frame);
+ mResultsFrame = (FrameLayout) root.findViewById(R.id.lb_results_frame);
+ mSearchBar = (SearchBar) mSearchFrame.findViewById(R.id.lb_search_bar);
+ mSearchBar.setSearchBarListener(new SearchBar.SearchBarListener() {
+ @Override
+ public void onSearchQueryChanged(String searchQuery) {
+ if (null != mProvider) {
+ retrieveResults(searchQuery);
+ } else {
+ mPendingQuery = searchQuery;
+ }
+ }
+ });
+ Bundle args = getArguments();
+ if (null != args) {
+ String query = args.getString(ARG_QUERY, "");
+ mSearchBar.setSearchQuery(query);
+ }
+
+ // Inject the RowsFragment in the results container
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.lb_results_container, mRowsFragment).commit();
+ return root;
+ }
+
+ /**
+ * Set the search provider, which is responsible for returning items given
+ * a search term
+ *
+ * @param searchResultProvider the search provider
+ */
+ public void setSearchResultProvider(SearchResultProvider searchResultProvider) {
+ mProvider = searchResultProvider;
+ onSetSearchResultProvider();
+ }
+
+ /**
+ * Set background parameters
+ * @param params the background parameters
+ */
+ public void setBackgroundParams(BackgroundParams params) {
+ mRowsFragment.setBackgroundParams(params);
+ }
+
+ /**
+ * Sets an item selection listener.
+ * @param listener the item selection listener
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mRowsFragment.setOnItemSelectedListener(listener);
+ }
+
+ private void retrieveResults(String searchQuery) {
+ ObjectAdapter adapter = mProvider.results(searchQuery);
+ mRowsFragment.setAdapter(adapter);
+ mResultsFrame.setVisibility(View.VISIBLE);
+ }
+
+ private void onSetSearchResultProvider() {
+ executePendingQuery();
+ }
+
+ private void executePendingQuery() {
+ if (null != mPendingQuery) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ retrieveResults(mPendingQuery);
+ mPendingQuery = null;
+ }
+ });
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
new file mode 100644
index 0000000..cb3f253
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2014 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 android.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import android.support.v17.leanback.R;
+
+/**
+ * SearchBar is a search widget.
+ */
+public class SearchBar extends RelativeLayout {
+ private static final String TAG = SearchBar.class.getSimpleName();
+
+ /**
+ * Listener for search query changes
+ */
+ public interface SearchBarListener {
+
+ /**
+ * Method invoked when the search bar detects a change in the query
+ * @param searchQuery the current full query
+ */
+ public void onSearchQueryChanged(String searchQuery);
+ }
+
+ private SearchBarListener mSearchBarListener;
+ private EditText mSearchTextEditor;
+ private String mSearchQuery;
+ private final Handler mHandler = new Handler();
+
+ public SearchBar(Context context) {
+ this(context, null);
+ }
+
+ public SearchBar(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SearchBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mSearchQuery = "";
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mSearchTextEditor = (EditText)findViewById(R.id.lb_search_text_editor);
+ mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (hasFocus) {
+ showNativeKeyboard();
+ }
+ }
+ });
+ mSearchTextEditor.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ setSearchQuery(charSequence.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+
+ }
+ });
+ mSearchTextEditor.setFocusable(true);
+ mSearchTextEditor.setVisibility(VISIBLE);
+ mSearchTextEditor.requestFocus();
+ }
+
+ /**
+ * Set a listener for when the term search changes
+ * @param listener
+ */
+ public void setSearchBarListener(SearchBarListener listener) {
+ mSearchBarListener = listener;
+ }
+
+ /**
+ * Set the search query
+ * @param query the search query to use
+ */
+ public void setSearchQuery(String query) {
+ if (query.equals(mSearchQuery)) {
+ return;
+ }
+ mSearchQuery = query;
+ if (null != mSearchBarListener) {
+ mSearchBarListener.onSearchQueryChanged(mSearchQuery);
+ }
+ }
+
+ protected void showNativeKeyboard() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSearchTextEditor.requestFocusFromTouch();
+ mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN,
+ mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
+ mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(), MotionEvent.ACTION_UP,
+ mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
+ }
+ });
+ }
+
+}