/* * Copyright 2015. Appsi Mobile * * 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.appsimobile.appsii.module.apps; import android.animation.Animator; import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.LayoutAnimationController; import android.view.animation.TranslateAnimation; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.TextView; import com.appsimobile.appsii.AnimatorAdapter; import com.appsimobile.appsii.R; import java.util.ArrayList; import java.util.List; /** * Created by nick on 09/01/15. */ class BottomSheetHelper { final Context mContext; View mOverlay; View mBottomSheet; View mContentView; ImageView mAppIconView; TextView mAppTitleView; ImageView mCloseButton; View mTitleDivider; RecyclerView mTagsRecycler; View mAddTagView; AppEntry mAppEntry; final AppsController mAppsController; final AppTagAdapter mAdapter; final float mCellHeight; int mMaxTranslationY; int mMinTranslationY; View mBottomSheetFooter; private final Animator.AnimatorListener mOnClosedListener = new AnimatorAdapter() { @Override public void onAnimationEnd(Animator animation) { mOverlay.setVisibility(View.GONE); mBottomSheet.setVisibility(View.INVISIBLE); } }; BottomSheetHelper(Context context, AppsController appsController) { mContext = context; mAppsController = appsController; mAdapter = new AppTagAdapter(); TypedArray t = context.obtainStyledAttributes( new int[]{android.R.attr.listPreferredItemHeightSmall} ); mCellHeight = t.getDimension(0, 0); t.recycle(); } public void onViewCreated(View parent) { mBottomSheetFooter = parent.findViewById(R.id.bottom_sheet_footer); mOverlay = parent.findViewById(R.id.bottom_sheet_overlay); mContentView = parent.findViewById(R.id.bottom_sheet_content); mBottomSheet = parent.findViewById(R.id.bottom_sheet); mAppIconView = (ImageView) parent.findViewById(R.id.sheet_app_icon); mAppTitleView = (TextView) parent.findViewById(R.id.sheet_app_title); mCloseButton = (ImageView) parent.findViewById(R.id.close_sheet); mTitleDivider = parent.findViewById(R.id.title_divider); mTagsRecycler = (RecyclerView) parent.findViewById(R.id.tag_recycler_view); mTagsRecycler.setLayoutManager(new LinearLayoutManager(parent.getContext())); mTagsRecycler.setAdapter(mAdapter); mTagsRecycler.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { /** * The original touched y position */ float mStartY; /** * The translation when we first touched the view. Used to calculate the * translation to apply */ float mStartTy; /** * The touchslop */ float mTouchSlop; /** * True if we decided to capture the event. Used to know if up/cancel * events must be handled. */ boolean mCaptured; /** * An event is marked as ignored if the touch-slop was passed we we decided, * not to handle the touch in favor of a scroll */ boolean mIgnoring; /** * True when we are closing because of the gesture. */ boolean mClosing; { mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { int action = e.getAction(); float rawY = e.getRawY(); // Reset the state on a down event if (action == MotionEvent.ACTION_DOWN) { mCaptured = false; mClosing = false; mIgnoring = false; mStartY = rawY; mStartTy = mContentView.getTranslationY(); } // calculate the delta, and when this is a move bail out when // the touch-slop was not reached float delta = Math.abs(mStartY - rawY); if (action == MotionEvent.ACTION_MOVE && delta <= mTouchSlop) { return false; } // immediately ignore the event when it is marked as an event // that should not be handled if (action == MotionEvent.ACTION_MOVE && mIgnoring) { return false; } // if the event is up or cancel return true if it was captured. // If we don't do this, this will make the checkboxes not // clickable if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { return mCaptured; } // Only move events will be handled below. So return false when it // is not a move event. if (action != MotionEvent.ACTION_MOVE) { return false; } // is the view maximally expanded boolean expanded = mStartTy == mMinTranslationY; // is the view maximally collapsed boolean collapsed = mStartTy == mMaxTranslationY; // true if we are moving down the screen. // useful when combining with the recycler-view // ability to scroll up or down boolean down = rawY > mStartY; if (expanded && down) { // if the view can scroll up, we ignore the event so the // recycler will be able to scroll. // This could be rewritten to start scrolling after full // collapse, by clearing mIgnoring earlier on if we got // into this state before boolean canScrollUp = canRecyclerScrollUp(mTagsRecycler); if (!canScrollUp) { mClosing = true; mCaptured = true; return true; } else { mIgnoring = true; } return false; } if (down && !canRecyclerScrollUp(mTagsRecycler)) { mCaptured = true; return true; } if (collapsed && !down) { mCaptured = true; return true; } if (!down && !canRecyclerScrollDown(mTagsRecycler)) { mCaptured = true; return true; } mIgnoring = true; return false; } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { int action = e.getAction(); float newY = e.getRawY(); float amount = mStartY - newY; float ty = mStartTy - amount; switch (action) { case MotionEvent.ACTION_MOVE: if (ty > mMaxTranslationY && !mClosing) { ty = mMaxTranslationY; } if (ty < mMinTranslationY) { ty = mMinTranslationY; } mContentView.setTranslationY(ty); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: boolean closing = ty > mMaxTranslationY && mClosing; snapToPosition(newY < mStartY, closing); break; } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } }); mAddTagView = parent.findViewById(R.id.add_tag); mAddTagView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mAppsController.onAddAppToNewTag(mAppEntry); close(); } }); mCloseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { close(); } }); mOverlay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { close(); } }); mContentView.setClickable(true); Animation animation = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_PARENT, 1, Animation.RELATIVE_TO_PARENT, 0); animation.setDuration(450); animation.setInterpolator(new DecelerateInterpolator()); LayoutAnimationController controller = new LayoutAnimationController(animation) { @Override protected long getDelayForView(@NonNull View view) { int top = view.getTop(); float pct = top / (float) mContentView.getHeight(); return (int) (pct * 200); } }; ((ViewGroup) mContentView).setLayoutAnimation(controller); } static boolean canRecyclerScrollUp(RecyclerView recyclerView) { View view = recyclerView.getChildAt(0); if (view == null) return false; boolean isFirst = recyclerView.getChildLayoutPosition(view) == 0; if (!isFirst) return true; int top = view.getTop(); //noinspection RedundantIfStatement if (top < 0) return true; return false; } static boolean canRecyclerScrollDown(RecyclerView recyclerView) { View view = recyclerView.getChildAt(recyclerView.getChildCount() - 1); if (view == null) return false; int lastItemPosition = recyclerView.getAdapter().getItemCount() - 1; boolean isLast = recyclerView.getChildLayoutPosition(view) == lastItemPosition; if (!isLast) return true; int bottom = view.getBottom(); //noinspection RedundantIfStatement if (bottom > recyclerView.getHeight()) return true; return false; } void snapToPosition(boolean expand, boolean closing) { if (expand) { mContentView.animate().translationY(mMinTranslationY); } else { if (closing) { close(); } mContentView.animate().translationY(mMaxTranslationY); } } void close() { mBottomSheet.animate() .translationY(mBottomSheet.getHeight()) .setListener(mOnClosedListener); mOverlay.animate().alpha(0); } public AppEntry getBoundAppEntry() { return mAppEntry; } void setAppTags(List<AppTag> allTags) { mAdapter.setItems(allTags); } public void updateAppliedTags(@Nullable List<TaggedApp> appliedTags) { mAdapter.setAppliedTags(appliedTags); } public void show(AppEntry entry, @Nullable List<TaggedApp> appliedTags) { setupAnimationValues(); mAppEntry = entry; mAdapter.setAppliedTags(appliedTags); mAppIconView.setImageDrawable(entry.getIcon(mContext.getPackageManager())); mAppTitleView.setText(entry.getLabel()); mBottomSheet.setVisibility(View.VISIBLE); mOverlay.setVisibility(View.VISIBLE); mOverlay.setAlpha(0); mOverlay.animate().alpha(1); mBottomSheet.setTranslationY(mBottomSheet.getHeight()); mBottomSheet.animate().translationY(0).setListener(null); ((ViewGroup) mContentView).startLayoutAnimation(); } private void setupAnimationValues() { int totalHeight = mOverlay.getHeight(); int count = mAdapter.getItemCount(); float density = mTagsRecycler.getResources().getDisplayMetrics().density; float headerHeight = 64 * density; int requiredHeightForAllItems = (int) (( mCellHeight * (count < 2 ? 2 : count)) + headerHeight); // this is the amount of items shown in collapsed size. // in case we have 2 or less tags, show two cells. // otherwise show 2.5 cells so the user can see the // view is scrollable. int minDisplayHeight; if (count <= 2) { minDisplayHeight = (int) ((mCellHeight * 2) + headerHeight); } else { minDisplayHeight = (int) ((mCellHeight * 2.5f) + headerHeight); } int h = mContentView.getHeight(); int maxHeight = Math.min(totalHeight, requiredHeightForAllItems); mMinTranslationY = h - maxHeight; // int statusBarHeight = (int) (24 * density); int statusBarHeight = 0; if (mMinTranslationY < statusBarHeight) { mMinTranslationY = statusBarHeight; } // int startHeight = (int) (4f * mCellHeight); int ty = h - minDisplayHeight; mMaxTranslationY = ty; mContentView.setTranslationY(ty); } class AppTagAdapter extends RecyclerView.Adapter<AppTagViewHolder> { final List<AppTag> mAppTags = new ArrayList<>(); final List<TaggedApp> mAppliedAppTags = new ArrayList<>(); AppTagAdapter() { setHasStableIds(true); } @Override public AppTagViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final View view = inflater.inflate(R.layout.list_item_tag_checkbox, parent, false); return new AppTagViewHolder(view, mAppsController); } @Override public void onBindViewHolder(AppTagViewHolder holder, int position) { AppTag tag = mAppTags.get(position); holder.bind(tag, mAppliedAppTags); } @Override public long getItemId(int position) { return mAppTags.get(position).id; } @Override public int getItemCount() { return mAppTags.size(); } public void setAppliedTags(@Nullable List<TaggedApp> appliedAppTags) { mAppliedAppTags.clear(); if (appliedAppTags != null) { mAppliedAppTags.addAll(appliedAppTags); } notifyDataSetChanged(); } public void setItems(List<AppTag> allTags) { mAppTags.clear(); int N = allTags.size(); for (int i = 0; i < N; i++) { AppTag tag = allTags.get(i); if (tag.tagType == AppsContract.TagColumns.TAG_TYPE_USER) { mAppTags.add(tag); } } notifyDataSetChanged(); } } class AppTagViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener { final AppsController mAppsController; private final TextView mLabel; private final CheckBox mCheckBox; AppTag mAppTag; @Nullable TaggedApp mTaggedApp; public AppTagViewHolder(View itemView, AppsController appsController) { super(itemView); mAppsController = appsController; mLabel = (TextView) itemView.findViewById(R.id.checkbox_label); mCheckBox = (CheckBox) itemView.findViewById(R.id.checkbox); } void bind(final AppTag appTag, List<TaggedApp> appliedTags) { mAppTag = appTag; final TaggedApp taggedApp = getTaggedApp(appTag, appliedTags); mTaggedApp = taggedApp; boolean checked = taggedApp != null; mLabel.setText(appTag.title); mCheckBox.setOnCheckedChangeListener(null); mCheckBox.setChecked(checked); mCheckBox.setOnCheckedChangeListener(this); } /** * Gets the TaggedApp for the given AppTag. Will return null if * the app-entry was not tagged with the given app-tag */ @Nullable TaggedApp getTaggedApp(AppTag appTag, List<TaggedApp> appliedTags) { int count = appliedTags.size(); for (int i = 0; i < count; i++) { TaggedApp taggedApp = appliedTags.get(i); if (TextUtils.equals(taggedApp.mTagName, appTag.title)) { return taggedApp; } } return null; } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { mAppsController.onAddAppToTag(mAppTag, mAppEntry); } else { mAppsController.onRemoveAppFromTag(mTaggedApp); } } } }