1 package com.example.android.leanback; 2 3 import static com.example.android.leanback.CardPresenter.CONTENT; 4 import static com.example.android.leanback.CardPresenter.IMAGE; 5 import static com.example.android.leanback.CardPresenter.TITLE; 6 7 import android.content.Context; 8 import android.content.Intent; 9 import android.os.Bundle; 10 import android.os.Handler; 11 import android.text.TextUtils; 12 import android.util.Log; 13 14 import androidx.core.app.ActivityOptionsCompat; 15 import androidx.core.content.res.ResourcesCompat; 16 import androidx.leanback.widget.ArrayObjectAdapter; 17 import androidx.leanback.widget.DiffCallback; 18 import androidx.leanback.widget.HeaderItem; 19 import androidx.leanback.widget.ImageCardView; 20 import androidx.leanback.widget.ListRow; 21 import androidx.leanback.widget.ListRowPresenter; 22 import androidx.leanback.widget.ObjectAdapter; 23 import androidx.leanback.widget.OnItemViewClickedListener; 24 import androidx.leanback.widget.Presenter; 25 import androidx.leanback.widget.Row; 26 import androidx.leanback.widget.RowPresenter; 27 28 import org.jspecify.annotations.NonNull; 29 import org.jspecify.annotations.Nullable; 30 31 import java.util.ArrayList; 32 33 public class SearchFragment extends androidx.leanback.app.SearchFragment 34 implements androidx.leanback.app.SearchFragment.SearchResultProvider { 35 private static final String TAG = "leanback.SearchFragment"; 36 private static final int NUM_ROWS = 3; 37 private static final int SEARCH_DELAY_MS = 1000; 38 39 private ArrayObjectAdapter mRowsAdapter; 40 private Handler mHandler = new Handler(); 41 private String mQuery; 42 43 // Flag to represent if data set one is presented in the fragment 44 private boolean mIsDataSetOnePresented; 45 46 // Adapter for first row 47 private ArrayObjectAdapter mFirstRowAdapter; 48 49 // The diff callback which defines the standard to judge if two items are the same or if 50 // two items have the same content. 51 private DiffCallback<PhotoItem> mDiffCallback = new DiffCallback<PhotoItem>() { 52 53 // when two photo items have the same id, they are the same from adapter's 54 // perspective 55 @Override 56 public boolean areItemsTheSame(@NonNull PhotoItem oldItem, @NonNull PhotoItem newItem) { 57 return oldItem.getId() == newItem.getId(); 58 } 59 60 // when two photo items is equal to each other (based on the equal method defined in 61 // PhotoItem), they have the same content. 62 @Override 63 public boolean areContentsTheSame(@NonNull PhotoItem oldItem, @NonNull PhotoItem newItem) { 64 return oldItem.equals(newItem); 65 } 66 67 @Override 68 public @Nullable Object getChangePayload(@NonNull PhotoItem oldItem, 69 @NonNull PhotoItem newItem) { 70 Bundle diff = new Bundle(); 71 if (oldItem.getImageResourceId() 72 != newItem.getImageResourceId()) { 73 diff.putLong(IMAGE, newItem.getImageResourceId()); 74 } 75 76 if (oldItem.getTitle() != null && newItem.getTitle() != null 77 && !oldItem.getTitle().equals(newItem.getTitle())) { 78 diff.putString(TITLE, newItem.getTitle()); 79 } 80 81 if (oldItem.getContent() != null && newItem.getContent() != null 82 && !oldItem.getContent().equals(newItem.getContent())) { 83 diff.putString(CONTENT, newItem.getContent()); 84 } 85 return diff; 86 } 87 }; 88 89 @Override onCreate(Bundle savedInstanceState)90 public void onCreate(Bundle savedInstanceState) { 91 super.onCreate(savedInstanceState); 92 93 mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); 94 95 final Context context = getActivity(); 96 setBadgeDrawable(ResourcesCompat.getDrawable(context.getResources(), 97 R.drawable.ic_title, context.getTheme())); 98 setTitle("Leanback Sample App"); 99 setSearchResultProvider(this); 100 setOnItemViewClickedListener(new ItemViewClickedListener()); 101 } 102 103 @Override getResultsAdapter()104 public ObjectAdapter getResultsAdapter() { 105 return mRowsAdapter; 106 } 107 108 @Override onQueryTextChange(String newQuery)109 public boolean onQueryTextChange(String newQuery) { 110 Log.i(TAG, String.format("Search Query Text Change %s", newQuery)); 111 mRowsAdapter.clear(); 112 loadQuery(newQuery); 113 return true; 114 } 115 116 @Override onQueryTextSubmit(String query)117 public boolean onQueryTextSubmit(String query) { 118 Log.i(TAG, String.format("Search Query Text Submit %s", query)); 119 mRowsAdapter.clear(); 120 loadQuery(query); 121 return true; 122 } 123 loadQuery(String query)124 private void loadQuery(String query) { 125 mQuery = query; 126 mHandler.removeCallbacks(mDelayedLoad); 127 if (!TextUtils.isEmpty(query) && !query.equals("nil")) { 128 mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS); 129 } 130 } 131 loadRows()132 private void loadRows() { 133 HeaderItem header = new HeaderItem(0, mQuery + " results row " + 0); 134 135 // Every time when the query event is fired, we will update the fake search result in the 136 // first row based on the flag mIsDataSetOnePresented flag. 137 // Also the first row adapter will only be created once so the animation will be triggered 138 // when the items in the adapter changed. 139 if (!mIsDataSetOnePresented) { 140 if (mFirstRowAdapter == null) { 141 mFirstRowAdapter = createFirstListRowAdapter(); 142 } else { 143 mFirstRowAdapter.setItems(createDataSetOneDebug(), mDiffCallback); 144 } 145 mIsDataSetOnePresented = true; 146 } else { 147 mFirstRowAdapter.setItems(createDataSetTwoDebug(), mDiffCallback); 148 mIsDataSetOnePresented = false; 149 } 150 mRowsAdapter.add(new ListRow(header, mFirstRowAdapter)); 151 for (int i = 1; i < NUM_ROWS + 1; ++i) { 152 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); 153 listRowAdapter.add(new PhotoItem("Hello world", R.drawable.gallery_photo_1)); 154 listRowAdapter.add(new PhotoItem("This is a test", R.drawable.gallery_photo_2)); 155 header = new HeaderItem(i, mQuery + " results row " + i); 156 mRowsAdapter.add(new ListRow(header, listRowAdapter)); 157 } 158 } 159 160 private Runnable mDelayedLoad = new Runnable() { 161 @Override 162 public void run() { 163 loadRows(); 164 } 165 }; 166 167 private final class ItemViewClickedListener implements OnItemViewClickedListener { 168 @Override onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)169 public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, 170 RowPresenter.ViewHolder rowViewHolder, Row row) { 171 Intent intent = new Intent(getActivity(), DetailsActivity.class); 172 intent.putExtra(DetailsActivity.EXTRA_ITEM, (PhotoItem) item); 173 174 Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( 175 getActivity(), 176 ((ImageCardView) itemViewHolder.view).getMainImageView(), 177 DetailsActivity.SHARED_ELEMENT_NAME).toBundle(); 178 getActivity().startActivity(intent, bundle); 179 } 180 } 181 182 createFirstListRowAdapter()183 private ArrayObjectAdapter createFirstListRowAdapter() { 184 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); 185 listRowAdapter.setItems(createDataSetOneDebug(), mDiffCallback); 186 mIsDataSetOnePresented = true; 187 return listRowAdapter; 188 } 189 createDataSetOneDebug()190 private ArrayList<PhotoItem> createDataSetOneDebug() { 191 ArrayList<PhotoItem> photoItems = new ArrayList<>(); 192 photoItems.add(new PhotoItem( 193 "Hello world", 194 R.drawable.gallery_photo_1, 195 1)); 196 return photoItems; 197 } 198 199 /** 200 * Create a new data set (data set one) for the last row of this browse fragment. It will be 201 * changed by another set of data when user click one of the photo items in the list. 202 * Different with other rows in the browsing fragment, the photo item in last row all have been 203 * allocated with a unique id. And the id will be used to jduge if two photo items are the same 204 * or not. 205 * 206 * @return List of photoItem 207 */ createDataSetTwoDebug()208 private ArrayList<PhotoItem> createDataSetTwoDebug() { 209 ArrayList<PhotoItem> photoItems = new ArrayList<>(); 210 photoItems.add(new PhotoItem( 211 "Hello world Hello world", 212 R.drawable.gallery_photo_1, 213 1)); 214 return photoItems; 215 } 216 } 217