package net.xpece.material.navigationdrawer.list; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.support.annotation.AttrRes; import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.AdapterView; import android.widget.ListView; import net.xpece.material.navigationdrawer.R; import net.xpece.material.navigationdrawer.descriptors.CompositeNavigationItemDescriptor; import net.xpece.material.navigationdrawer.descriptors.NavigationItemDescriptor; import net.xpece.material.navigationdrawer.descriptors.NavigationSectionDescriptor; import net.xpece.material.navigationdrawer.internal.Utils; import java.util.ArrayList; import java.util.List; /** * Created by pechanecjr on 14. 12. 2014. */ abstract class NavigationListFragmentDelegate implements AdapterView.OnItemClickListener, NavigationListFragmentImpl { public static final String TAG = NavigationListFragmentDelegate.class.getSimpleName(); private static final NavigationListFragmentCallbacks DUMMY_CALLBACKS = new NavigationListFragmentCallbacks() { @Override public void onNavigationItemSelected(View view, int position, int id, NavigationItemDescriptor item) { // } }; private NavigationListFragmentCallbacks mCallbacks = DUMMY_CALLBACKS; private View mView; private ListView mListView; private ViewGroup mPinnedContainer; private View mPinnedDivider; private int mTheFix = 0; private boolean mShouldPinnedSectionHaveBackground = false; private Drawable mBackground = null; private Drawable mPinnedSectionBackground = null; private int mTheme; private LayoutInflater mInflater; private final ViewTreeObserver.OnGlobalLayoutListener mPinnedContainerOnGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // Problem: in portrait the list has extra space at the bottom (5dp, 8px) // Solution: calculate the extra space and subtract it from padding int fix = 0; final int paddingBottom = mListView.getPaddingBottom(); // final int lastVisible = mListView.getLastVisiblePosition(); // final int lastPosition = mListView.getAdapter().getCount() - 1; // mAdapter.getCount() - 1; //// timber("listVisible=%s, lastPosition=%s", lastVisible, lastPosition); // if (lastVisible == lastPosition) { // final int listHeight = mListView.getMeasuredHeight() - paddingBottom - mListView.getPaddingTop(); // final int lastBottom = mListView.getChildAt(mListView.getLastVisiblePosition() - mListView.getFirstVisiblePosition()).getBottom(); //// timber("listHeight=%s, lastBottom=%s", listHeight, lastBottom); // // if last item ends before the list ends there's extra space // if (lastBottom < listHeight) { // if (paddingBottom == 0) mTheFix = Math.max(0, listHeight - lastBottom); // fix = mTheFix; //// timber("extraSpace=" + fix); // } // } // Cause: Transparent ColorDrawable used as divider had intrinsic height equal to -1 // which effectively moved each item 1px upwards. // modify padding only after pinned section has been measured and it changed // padding = pinned section height - listview extra space - 1dp divider alignment final int pinnedHeight = mPinnedContainer.getMeasuredHeight(); // timber("pinnedHeight=%s", pinnedHeight); final int targetPadding = pinnedHeight - fix - Utils.dpToPixelOffset(getActivity(), 1); // timber("targetPadding=%s, paddingBottom=%s", targetPadding, paddingBottom); if (paddingBottom != targetPadding) { mListView.setPadding(0, 0, 0, targetPadding); } // if pinned section is at the very bottom elevate it if (getView() == null) return; final int parentHeight = getView().getHeight(); final int pinnedBottom = mPinnedContainer.getBottom(); if (pinnedBottom >= parentHeight) { // there is not enough room, the section will be pinned int colorBackground = Utils.getColor(getView().getContext(), android.R.attr.colorBackground, 0); if (Build.VERSION.SDK_INT < 21 || (colorBackground & 0xffffff) < 0xffffff / 2) { // on API lower than 21 and on dark theme show the line instead of shadow Utils.setElevation(mPinnedContainer, 0); mPinnedDivider.setVisibility(View.VISIBLE); mShouldPinnedSectionHaveBackground = true; // the views would be seen about to scroll in } else { // on light theme on API 21 show shadow instead of line Utils.setElevation(mPinnedContainer, getActivity().getResources().getDimension(R.dimen.mnd_unit)); mPinnedDivider.setVisibility(View.GONE); mShouldPinnedSectionHaveBackground = true; } } else { // there is enough room, the section will not be pinned Utils.setElevation(mPinnedContainer, 0); mPinnedDivider.setVisibility(View.VISIBLE); mShouldPinnedSectionHaveBackground = false; } updatePinnedSectionBackground(); if (paddingBottom == targetPadding) { // my work here is done Utils.removeOnGlobalLayoutListener(mPinnedContainer, mPinnedContainerOnGlobalLayoutListener); } } }; private NavigationListAdapter mAdapter; private int mLastSelected = -1; private boolean mReselectEnabled = true; private List<NavigationSectionDescriptor> mSections = new ArrayList<>(0); private List<CompositeNavigationItemDescriptor> mPinnedSection = null; private View mHeader = null; public NavigationListFragmentDelegate() { } public abstract Activity getActivity(); public abstract View getView(); @Override public void onAttach(Activity activity) { mCallbacks = (NavigationListFragmentCallbacks) activity; } @Override public void onDetach() { mCallbacks = DUMMY_CALLBACKS; } @Override public void onSaveInstanceState(Bundle outState) { outState.putInt("mLastSelected", mLastSelected); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { if (mTheme > 0) { Context newContext = new ContextThemeWrapper(inflater.getContext(), mTheme); inflater = inflater.cloneInContext(newContext); } mInflater = inflater; mView = inflater.inflate(R.layout.mnd_list, container, false); mListView = (ListView) mView.findViewById(R.id.mnd_list); mPinnedContainer = (ViewGroup) mView.findViewById(R.id.mnd_section_pinned); mPinnedDivider = mView.findViewById(R.id.mnd_divider_pinned); return mView; } @Override public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) { Context context = view.getContext(); mPinnedDivider.setBackgroundColor(Utils.createDividerColor(context)); mListView.setOnItemClickListener(this); mListView.setSelector(Utils.getSelectorDrawable(context)); if (savedInstanceState != null) { mLastSelected = savedInstanceState.getInt("mLastSelected"); } } @Override public void onDestroyView() { mInflater = null; mView = null; mListView = null; mPinnedContainer = null; mPinnedDivider = null; } @Override public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.NavigationListFragment); mTheme = a.getResourceId(R.styleable.NavigationListFragment_android_theme, 0); a.recycle(); } @Override public LayoutInflater getLayoutInflater2() { return mInflater; } @Override public void setItems(List<? extends CompositeNavigationItemDescriptor> items) { NavigationSectionDescriptor section = new NavigationSectionDescriptor().addItems(items); List<NavigationSectionDescriptor> sections = new ArrayList<>(1); sections.add(section); setSections(sections); } /** * Set all sections that would be shown in the navigation list except optional pinned section. * * @param sections */ @Override public void setSections(List<NavigationSectionDescriptor> sections) { mSections = sections; updateSections(); } private void updateSections() { if (getView() == null) return; mAdapter = new NavigationListAdapter(mSections); mAdapter.setReselectEnabled(mReselectEnabled); mAdapter.setActivatedItem(mLastSelected - getHeaderViewsCount()); mListView.setAdapter(mAdapter); mPinnedContainer.getViewTreeObserver().addOnGlobalLayoutListener(mPinnedContainerOnGlobalLayoutListener); } /** * Use this method to set a section that would be pinned at the bottom of the screen when there's * not enough room for the whole list to show. Typically this section would contain * {@code Settings} and {@code Help & feedback} menu items. * * @param section */ @Override public void setPinnedSection(NavigationSectionDescriptor section) { if (section == null || !section.equals(mPinnedSection)) { mPinnedSection = section; updatePinnedSection(); } } private void updatePinnedSection() { if (getView() == null) return; Drawable selector = Utils.getSelectorDrawable(getView().getContext()); final int offset = 2; // plus 1 for the divider view plus 1 for padding int targetCount = mPinnedSection == null ? 0 : mPinnedSection.size(); // while (mPinnedContainer.getChildCount() > targetCount + offset) { // View view = mPinnedContainer.getChildAt(offset); // view.setOnClickListener(null); // mPinnedContainer.removeView(view); // } // TODO temporarily removing all, theres no knowing precise AbsNavItemDesc subtype while (mPinnedContainer.getChildCount() > offset) { View view = mPinnedContainer.getChildAt(offset); view.setOnClickListener(null); mPinnedContainer.removeView(view); } int currentCount = mPinnedContainer.getChildCount() - offset; for (int i = 0; i < targetCount; i++) { final CompositeNavigationItemDescriptor item = mPinnedSection.get(i); final View view; if (i < currentCount) { view = mPinnedContainer.getChildAt(i + offset); item.bindView(view, false); } else { Context context = getLayoutInflater2().getContext(); view = item.createView(context, mPinnedContainer); item.bindView(view, false); mPinnedContainer.addView(view); } Utils.setBackground(view, selector.getConstantState().newDrawable()); final int relativePosition = i; view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // mCallbacks.onNavigationItemSelected(v, mAdapter.getCount() + relativePosition, item.getId()); onItemClick(v, mAdapter.getCount() + relativePosition, item.getId(), item); } }); } if (targetCount > 0) { mPinnedContainer.setVisibility(View.VISIBLE); } else { mPinnedContainer.setVisibility(View.GONE); } mPinnedContainer.getViewTreeObserver().addOnGlobalLayoutListener(mPinnedContainerOnGlobalLayoutListener); } /** * Use this method to set a header view. This header is selectable by default so you can use it as * a no-op close drawer button. Alternatively you would set up an {@link android.view.View.OnClickListener} * beforehand. <em>The header view is not managed by this class, it's completely in your hands.</em> * * @param view */ @Override public void setHeaderView(View view, boolean clickable) { if (view == mHeader) return; if (mHeader != null) { mListView.removeHeaderView(mHeader); } if (view != null) { if (mListView != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { if (mListView.getAdapter() != null) mListView.setAdapter(null); mListView.addHeaderView(view, null, clickable); if (mAdapter != null) mListView.setAdapter(mAdapter); } else { mListView.addHeaderView(view, null, clickable); } } } mHeader = view; } /** * Call this method when the source data set has changed, e.g. when you changed badges. */ @Override public void notifyDataSetChanged() { if (mAdapter != null) { mAdapter.notifyDataSetChanged(); } } /** * Use this to set a color as the navigation list background. * Remember not to use translucent or transparent colors (pinned section has to have an opaque background). * * @param color */ @Override public void setBackgroundColor(int color) { if (getView() != null) { // mView.setBackgroundColor(color); // mPinnedContainer.setBackgroundColor(color); Drawable d = new ColorDrawable(color); setBackground(d); } } /** * Use this to set a drawable as the navigation list background. * Remember not to use vertical gradients (pinned section has to have an opaque background). * * @param drawable */ @Override public void setBackground(Drawable drawable) { if (getView() != null) { // Utils.setBackground(mView, drawable); // Utils.setBackground(mPinnedContainer, drawable); mBackground = drawable; updateBackground(); } } /** * Use this to resolve a drawable or color resource ID as a drawable and set it as the navigation list background. * Remember not to use vertical gradients (pinned section has to have an opaque background). * * @param resource */ @Override public void setBackgroundResource(@DrawableRes @ColorRes int resource) { if (getView() != null) { // mView.setBackgroundResource(resource); // mPinnedContainer.setBackgroundResource(resource); Drawable d = Utils.getDrawableRes(mView.getContext(), resource); setBackground(d); } } /** * Use this to resolve an attribute as a drawable and set it as the navigation list background. * Remember not to use vertical gradients (pinned section has to have an opaque background). * * @param attr */ @Override public void setBackgroundAttr(@AttrRes int attr) { if (getView() != null) { Drawable d = Utils.getDrawableAttr(mView.getContext(), attr); setBackground(d); } } /** * Use this method for marking selected item from outside. Typically you call this in * {@link android.app.Activity#onPostCreate(android.os.Bundle)} after you determine which * section was selected previously or by default. * * @param id */ @Override public void setSelectedItem(int id) { if (mAdapter != null) { int position = mAdapter.getPositionById(id); if (position >= 0) { if (trySelectPosition(position)) { scrollToPosition(position); } } } else { throw new IllegalStateException("No adapter yet!"); } } private void scrollToPosition(int position) { int last = mListView.getLastVisiblePosition(); int first = mListView.getFirstVisiblePosition(); if (last < position) { mListView.setSelection(position - (last - first) - 1); } else if (first > position) { mListView.setSelection(position - 1); } } private boolean trySelectPosition(final int itemPosition) { final int listPosition = itemPosition + getHeaderViewsCount(); // if (listPosition == mLastSelected) return; if (itemPosition < 0) { // this should never happen // selectPosition(-1, mLastSelected); // mAdapter.setActivatedItem(-1); // mLastSelected = -1; // return; throw new IllegalArgumentException("Position to select is less than 0."); } CompositeNavigationItemDescriptor item = (CompositeNavigationItemDescriptor) mAdapter.getItem(itemPosition); if (item != null && item.isSticky()) { // Utils.timber(TAG, "item=" + item + ", itemPosition=" + itemPosition + ", listPosition=" + listPosition); mAdapter.setActivatedItem(itemPosition); selectPosition(listPosition, mLastSelected); mLastSelected = listPosition; return true; } else { selectPosition(mLastSelected, listPosition); return false; } } private void selectPosition(int select, int deselect) { if (deselect >= 0) mListView.setItemChecked(deselect, false); if (select >= 0) mListView.setItemChecked(mLastSelected, true); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { CompositeNavigationItemDescriptor item = (CompositeNavigationItemDescriptor) parent.getItemAtPosition(position); onItemClick(view, position, item != null ? item.getId() : (int) id, item); } private void onItemClick(View view, int position, int id, CompositeNavigationItemDescriptor item) { // header views and items from pinned section are not sticky, don't even try if (position < getHeaderViewsCount()) { // || position > mListView.getHeaderViewsCount() + mAdapter.getCount()) { selectPosition(mLastSelected, position); } else { final int itemPosition = position - getHeaderViewsCount(); trySelectPosition(itemPosition); } if (item == null || !item.onClick(view)) { mCallbacks.onNavigationItemSelected(view, position, id, item); } } private int getHeaderViewsCount() {return mListView.getHeaderViewsCount();} private void updateBackground() { Utils.setBackground(mView, mBackground); mPinnedSectionBackground = mBackground.getConstantState().newDrawable(); updatePinnedSectionBackground(); } private void updatePinnedSectionBackground() { if (mShouldPinnedSectionHaveBackground) { Utils.setBackground(mPinnedContainer, mPinnedSectionBackground); } else { mPinnedContainer.setBackgroundResource(0); } } public void setReselectEnabled(boolean reselectEnabled) { mReselectEnabled = reselectEnabled; if (mAdapter != null) { mAdapter.setReselectEnabled(reselectEnabled); } } }