New version of Recommender
There is a new version of Recommender in the play sore. The main change is that I’ve added a Bottom Sheet using the Design Support Library. The Bottom Sheet is used to display the list of categories that you have currently used. Category is a free text field but is used to group recommendations so its best to try and avoid misspelt duplicate. The list of current categories is displayed to to try and discourage duplication. It looks like this
Its an extension of BottomSheetDialogFragment. Triggered by tapping the down arrow on the Category field. Its modal as I wanted you to either be selecting a current category or entering a new one. I found doing both at the same time made the screen to cluttered and I didn't want to break the flow by forcing you to go to a separate screen to enter a category.
The dialog that goes in the BottomSheet fragment is pretty straightforward the layout looks like this
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:layout_width="fill_parent" android:layout_height="1dp" android:background="@color/color_divider" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/category_fragment_title" android:textColor="@color/color_accent" android:paddingLeft="2dp" android:paddingRight="2dp" android:textSize="16sp"/> <ListView android:id="@+id/category_list" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> </LinearLayout>
Then I just put an ImageButton down and called showBottomSheet() on an instance of a CategoryBottomSheetFragment
public class CategoryBottomSheetFragment extends BottomSheetDialogFragment { private IEditRecommendationComponent component; @Inject protected IRecommendationRepository repository; @Inject protected ILoggerFactory logger; @Inject protected ITextUtils textUtils; @BindView(R.id.category_list) protected ListView categoryList; private CategoryAdapter categoryAdapter; private Subscription categorySubscription = null; private BottomSheetBehavior bottomSheetBehavior; private RecommendationEditFragment editFragment; private BottomSheetBehavior.BottomSheetCallback bottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { dismiss(); } } @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { } }; private IEditRecommendationComponent getComponent() { if (component == null) { component = DaggerIEditRecommendationComponent.builder() .iApplicationComponent(getApplicationComponent()) .baseActivityModule(new BaseActivityModule(getActivity())) .editRecommendationModule(new EditRecommendationModule()) .build(); } return component; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getComponent().inject(this); } @Override public void setupDialog(Dialog dialog, int style) { super.setupDialog(dialog, style); View contentView = View.inflate(getContext(), R.layout.fragment_category_bottomsheet, null); ButterKnife.bind(this, contentView); dialog.setContentView(contentView); // lets set the size of the dialog to be half the height of the screen Display display = getActivity().getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); contentView.getLayoutParams().height = size.y / 2; CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) contentView.getParent()).getLayoutParams(); CoordinatorLayout.Behavior behavior = params.getBehavior(); if ( behavior != null && behavior instanceof BottomSheetBehavior ) { bottomSheetBehavior = (BottomSheetBehavior) behavior; bottomSheetBehavior.setBottomSheetCallback(bottomSheetBehaviorCallback); } else { throw new UnsupportedOperationException("cannot find bottomSheetBehaviour"); } categoryAdapter = new CategoryAdapter(this.getActivity(), textUtils); categoryList.setAdapter(categoryAdapter); categoryList.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { if (editFragment != null) { editFragment.setCategory(categoryAdapter.getItem(position)); } dismiss(); } }); } @Override public void onResume() { super.onResume(); categorySubscription = repository.getCategories(new Action1
>() { @Override public void call(List categories) { categoryAdapter.call(categories); } }); } @Override public void onPause() { super.onPause(); // stop any subscriptions if (categorySubscription != null) { categorySubscription.unsubscribe(); categorySubscription = null; } } @Override public void onStart() { super.onStart(); bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); } public void hideBottomSheet() { bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } public void showBottomSheet( FragmentManager supportFragmentManager, RecommendationEditFragment editFragment) { this.editFragment = editFragment; show(supportFragmentManager, getTag()); } }
For completeness the adapter looks like this
final public class CategoryAdapter extends BaseAdapter implements Action1
> { private final LayoutInflater inflater; private ITextUtils textUtils; private List items = Collections.emptyList(); public CategoryAdapter(Context context, ITextUtils textUtils) { this.textUtils = textUtils; inflater = LayoutInflater.from(context); } @Override public void call(List strings) { if (strings == null || strings.size() < 1) { return; } this.items = new ArrayList (strings.size()); for (String thisString : strings) { if (!textUtils.isEmpty(thisString)) { items.add(thisString); } } notifyDataSetChanged(); } @Override public int getCount() { return items == null ? 0 : items.size(); } @Override public String getItem(int position) { return items.get(position); } @Override public long getItemId(int position) { return getItem(position).hashCode(); } @Override public boolean hasStableIds() { return true; } @Override public View getView(int position, View rowView, ViewGroup parent) { if (rowView == null) { rowView = inflater.inflate(R.layout.list_item_category, parent, false); } ViewHolder holder = new ViewHolder(rowView); String thisCategory = items.get(position); holder.label.setText(thisCategory); return rowView; } static class ViewHolder { @BindView(R.id.category_row_label) TextView label; public ViewHolder(View view) { ButterKnife.bind(this, view); } } }
The layout for each line in the adapter can be as complex as you like but this one is pretty simple
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="?attr/selectableItemBackground" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="@dimen/listview_min_height" android:gravity="center_vertical" android:paddingLeft="@dimen/listview_padding" android:paddingRight="@dimen/listview_padding" android:paddingTop="@dimen/listview_item_padding" android:paddingBottom="@dimen/listview_item_padding" > <TextView android:id="@+id/category_row_label" android:layout_width="fill_parent" android:layout_height="26dip" android:layout_centerHorizontal="true" android:ellipsize="marquee" android:singleLine="true" android:text="@string/placeholder" android:textSize="16sp"/> </LinearLayout> </LinearLayout>
You can also look at the full source code to get a better idea of how it all hangs together