diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/DishPartAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/DishPartAdapter.kt index 95d4ceb..ac95dd9 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/DishPartAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/DishPartAdapter.kt @@ -12,11 +12,22 @@ import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace import com.shuwei.dish.match.utils.ext.visible -import java.text.DecimalFormat +import com.shuwei.dish.match.view.swipereveallayout.ViewBinderHelper class DishPartAdapter(list: MutableList) : BaseQuickAdapter(list) { + var onDeleteClick: ((position: Int) -> Unit)? = null + var onItemClick: ((position: Int) -> Unit)? = null + + private val viewBinderHelper = ViewBinderHelper().apply { + setOpenOnlyOne(true) + } + + fun closeAll() { + viewBinderHelper.closeAll() + } + inner class VH(var binding: ListItemDishCookBinding) : QuickViewHolder(binding.root) override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { @@ -27,33 +38,47 @@ class DishPartAdapter(list: MutableList) : override fun onBindViewHolder(holder: VH, position: Int, item: CookFoodGoodsEntity?) { holder.binding.run { - tvDishName.text = item!!.goodsName - tvDishType.text = if (item.materialType == 1) "主辅材:主材" else if (item.materialType == 2) "主辅材:辅材" else "" - //"${DecimalFormat("#").format(item.useWeight)}克" + val data = item ?: return + val itemId = data.goodsId ?: "${position}_${data.goodsName ?: ""}" + viewBinderHelper.bind(swipeRevealLayout, itemId) + // 暂时禁用侧滑能力,后续需要放开时改回 unlockSwipe + layoutDelete.visible() + viewBinderHelper.lockSwipe(itemId) + swipeRevealLayout.close(false) + layoutDelete.gone() + + layoutDelete.setOnClickListener { + onDeleteClick?.invoke(holder.bindingAdapterPosition) + } + clBlock.setOnClickListener { + onItemClick?.invoke(holder.bindingAdapterPosition) + } + + tvDishName.text = data.goodsName + tvDishType.text = if (data.materialType == 1) "主辅材:主材" else if (data.materialType == 2) "主辅材:辅材" else "" tvDishWeight.text = - if (item.useWeight == null || item.useWeight == 0.toDouble()) "" else "${item.useWeight!!.roundedOneDecimalPlace()}克" + if (data.useWeight == null || data.useWeight == 0.toDouble()) "" else "${data.useWeight!!.roundedOneDecimalPlace()}克" tvDishWeight.setTextColor( ContextCompat.getColor( context, - if (item.isSamplingPage) R.color.black999 else R.color.dish_green + if (data.isSamplingPage) R.color.black999 else R.color.dish_green ) ) ivOperateIcon.run { - if (item.isSamplingPage) gone() else visible() + if (data.isSamplingPage) gone() else visible() } ivClearIcon.visible() ivClearIcon.setImageResource( - if (item.isOriginalData) R.drawable.ic_dish_clear + if (data.isOriginalData) R.drawable.ic_dish_clear else R.drawable.ic_delete ) ivOperateIcon.setImageResource( - if (item.isSetFinished) R.drawable.ic_dish_selected - else R.drawable.ic_dish_unselected + if (data.isSetFinished) R.drawable.ic_dish_selected + else R.drawable.ic_dish_unselected ) - root.run { + clBlock.run { setBackgroundResource( - if (item.isItemClicked) R.drawable.shape_item_cook_dish + if (data.isItemClicked) R.drawable.shape_item_cook_dish else R.drawable.shape_white_fb_15_corners ) } diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt index 46b551d..e811a15 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/FoodListAdapter.kt @@ -9,6 +9,7 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder import com.shuwei.dish.match.R import com.shuwei.dish.match.databinding.ListItemFoodListBinding import com.shuwei.dish.match.entity.FoodRecord +import com.shuwei.dish.match.view.swipereveallayout.ViewBinderHelper import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.visible import java.text.DecimalFormat @@ -34,6 +35,29 @@ class FoodListAdapter( SAMPLING_MODE } + /** 删除按钮点击回调,由外部(Activity/Fragment)设置 */ + var onDeleteClick: ((position: Int) -> Unit)? = null + + /** + * item 主体点击回调,绑定在 mainView(clBlock)上,绕过 SwipeRevealLayout 的触摸拦截。 + * 外部使用此回调替代 setOnDebouncedItemClick。 + */ + var onItemClick: ((position: Int) -> Unit)? = null + + /** + * ViewBinderHelper 负责: + * - 同一时间只允许一个 item 展开(setOpenOnlyOne) + * - RecyclerView 复用时恢复开合状态 + */ + private val viewBinderHelper = ViewBinderHelper().apply { + setOpenOnlyOne(true) + } + + /** 关闭所有已展开的侧滑 item,供外部在点击空白区域时调用 */ + fun closeAll() { + viewBinderHelper.closeAll() + } + inner class VH(var binding: ListItemFoodListBinding) : QuickViewHolder(binding.root) override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { @@ -51,6 +75,34 @@ class FoodListAdapter( val totalWeight = item.totalWeight ?: 0.0 val foodWeight = item.foodWeight ?: 0.0 + // 用 foodId 作为唯一 key 恢复开合状态;不可滑动时锁定 + val itemId = item.foodId ?: position.toString() + viewBinderHelper.bind(swipeRevealLayout, itemId) + if (isCooking) { + viewBinderHelper.unlockSwipe(itemId) + layoutDelete.visible() + } else { + viewBinderHelper.lockSwipe(itemId) + // 非烹饪中不允许侧滑,关闭可能的复用残留状态并隐藏删除按钮 + swipeRevealLayout.close(false) + layoutDelete.gone() + } + // 绑定删除按钮回调 + layoutDelete.setOnClickListener { onDeleteClick?.invoke(holder.bindingAdapterPosition) } + // 点击主内容区域: + // 1) 非烹饪中:先收起所有侧滑,再继续触发点击 + // 2) 烹饪中:当前 item 若已展开则只收起,不触发点击 + clBlock.setOnClickListener { + if (!isCooking) { + viewBinderHelper.closeAll() + onItemClick?.invoke(holder.bindingAdapterPosition) + } else if (!swipeRevealLayout.isClosed) { + viewBinderHelper.closeAll() + } else { + onItemClick?.invoke(holder.bindingAdapterPosition) + } + } + tvDishName.text = item.foodName //烹饪中 if (isCooking) { diff --git a/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt index 3681655..2140939 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt @@ -11,7 +11,6 @@ import androidx.activity.addCallback import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.shuwei.dish.match.R import com.shuwei.dish.match.adapter.DishPartAdapter @@ -29,7 +28,6 @@ import com.shuwei.dish.match.utils.CameraUtils import com.shuwei.dish.match.utils.ImageUtil import com.shuwei.dish.match.utils.KeyboardUtil import com.shuwei.dish.match.utils.SpTool -import com.shuwei.dish.match.utils.SwipeCallback import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.ext.addOnActionSearchListener import com.shuwei.dish.match.utils.ext.clickWithDebounce @@ -354,12 +352,29 @@ class PrepareFoodActivity : BaseActivity() { private val list = mutableListOf() private val dishPartAdapter by lazy { DishPartAdapter(list = list).apply { - setOnItemClickListener { _, _, positon -> + val adapter = this + onItemClick = { positon -> list.forEachIndexed { index, entity -> entity.isItemClicked = index == positon } notifyDataSetChanged() } + onDeleteClick = onDeleteClick@{ positon -> + Log.d(TAG, "onFoodItemClick: ${list[positon].toJsonString()}") + if (list[positon].isNewDishType.not()) { + toast("只能删除刚添加的食材") + return@onDeleteClick + } + CommonDialog(this@PrepareFoodActivity) + .setTitle("删除确认") + .setContent("确定删除食材「${list[positon].goodsName}」吗?") + .setNegativeButton("取消") + .setPositiveButton("删除") { + adapter.removeAt(positon) + toast("已删除") + } + .show() + } addOnItemChildClickListener(R.id.ivClearIcon) { _, _, positon -> Log.d(TAG, "onFoodItemClick: ${list[positon].toJsonString()}") if (list[positon].isOriginalData) { @@ -371,7 +386,7 @@ class PrepareFoodActivity : BaseActivity() { notifyItemChanged(positon) return@addOnItemChildClickListener } - removeAt(positon) + adapter.removeAt(positon) } } } @@ -384,16 +399,17 @@ class PrepareFoodActivity : BaseActivity() { layoutManager = LinearLayoutManager(this@PrepareFoodActivity, LinearLayoutManager.VERTICAL, false) adapter = dishPartAdapter - val itemTouchHelper = ItemTouchHelper(SwipeCallback(dishPartAdapter) { position -> - if (list[position].isNewDishType.not()) { - toast("只能删除刚添加的食材") - dishPartAdapter.notifyItemChanged(position) - return@SwipeCallback + addOnItemTouchListener(object : androidx.recyclerview.widget.RecyclerView.SimpleOnItemTouchListener() { + override fun onInterceptTouchEvent( + rv: androidx.recyclerview.widget.RecyclerView, + e: android.view.MotionEvent + ): Boolean { + if (e.action == android.view.MotionEvent.ACTION_DOWN) { + dishPartAdapter.closeAll() + } + return false } - dishPartAdapter.removeAt(position) - toast("已删除") }) - itemTouchHelper.attachToRecyclerView(this) } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt index 3e18c32..ef5074b 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SamplingModeActivity.kt @@ -4,11 +4,11 @@ import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent import androidx.activity.addCallback import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.shuwei.dish.match.R import com.shuwei.dish.match.adapter.FoodListAdapter @@ -19,7 +19,6 @@ import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.utils.DateTimeUtil -import com.shuwei.dish.match.utils.SwipeCallback import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.toast @@ -162,7 +161,24 @@ class SamplingModeActivity : BaseActivity() { private val dishAdapter by lazy { FoodListAdapter(list = list, mode = FoodListAdapter.DisplayMode.SAMPLING_MODE).apply { isStateViewEnable = true - setOnItemClickListener { adapter, view, position -> + onDeleteClick = { position -> + CommonDialog(this@SamplingModeActivity) + .setTitle("删除确认") + .setContent("确定要删除「${list[position].foodName}」吗?") + .setNegativeButton("取消") + .setPositiveButton("删除") { + deleteCookFoodAndGoods(foodId = list[position].foodId ?: "") { + if (list.size > 1) { + removeAt(position) + } else { + loadEmptyView() + } + toast("已删除") + } + } + .show() + } + onItemClick = { position -> judgeDeviceConfig { onItemClick(position) } @@ -192,22 +208,15 @@ class SamplingModeActivity : BaseActivity() { layoutManager = LinearLayoutManager(this@SamplingModeActivity, LinearLayoutManager.VERTICAL, false) adapter = dishAdapter - val itemTouchHelper = ItemTouchHelper(SwipeCallback(dishAdapter) { position -> - if (list[position].isCooking.not()) { - toast("只能删除烹饪中的菜品") - dishAdapter.notifyItemChanged(position) - return@SwipeCallback - } - deleteCookFoodAndGoods(foodId = list[position].foodId ?: "") { - if (list.size > 1) { - dishAdapter.removeAt(position) - } else { - loadEmptyView() + // 点击列表空白区域时关闭已展开的侧滑 item + addOnItemTouchListener(object : androidx.recyclerview.widget.RecyclerView.SimpleOnItemTouchListener() { + override fun onInterceptTouchEvent(rv: androidx.recyclerview.widget.RecyclerView, e: MotionEvent): Boolean { + if (e.action == MotionEvent.ACTION_DOWN) { + dishAdapter.closeAll() } - toast("已删除") + return false } }) - itemTouchHelper.attachToRecyclerView(this) } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/DishListFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/DishListFragment.kt index 8f9942a..b955e58 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/DishListFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/DishListFragment.kt @@ -3,13 +3,12 @@ package com.shuwei.dish.match.ui.fragment import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent import android.view.ViewGroup import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager -import com.chad.library.adapter4.util.setOnDebouncedItemClick import com.shuwei.dish.match.adapter.FoodListAdapter import com.shuwei.dish.match.base.BaseApp import com.shuwei.dish.match.base.BaseFragment @@ -17,11 +16,11 @@ import com.shuwei.dish.match.databinding.FragmentDishListBinding import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.FoodRecord +import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.net.UiState import com.shuwei.dish.match.ui.PrepareFoodActivity import com.shuwei.dish.match.ui.CookingModeActivity import com.shuwei.dish.match.ui.SubmitFoodActivity -import com.shuwei.dish.match.utils.SwipeCallback import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.toast import java.io.Serializable @@ -54,12 +53,24 @@ class DishListFragment : BaseFragment() { private val dishAdapter by lazy { FoodListAdapter(list = list).apply { isStateViewEnable = true - setOnDebouncedItemClick { adapter, view, position -> - if (isAdded.not() || isVisible.not()) { - return@setOnDebouncedItemClick - } - activity.judgeDeviceConfig { - onItemClick(position) + onDeleteClick = { position -> + CommonDialog(activity) + .setTitle("删除确认") + .setContent("确定要删除「${list[position].foodName}」吗?") + .setNegativeButton("取消") + .setPositiveButton("删除") { + activity.deleteCookFoodAndGoods(foodId = list[position].foodId ?: "") { + removeAt(position) + activity.toast("已删除") + } + } + .show() + } + onItemClick = { position -> + if (isAdded && isVisible) { + activity.judgeDeviceConfig { + onItemClick(position) + } } } } @@ -100,18 +111,15 @@ class DishListFragment : BaseFragment() { layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) adapter = dishAdapter - val itemTouchHelper = ItemTouchHelper(SwipeCallback(dishAdapter) { position -> - if (list[position].isCooking.not()) { - activity.toast("只能删除烹饪中的菜品") - dishAdapter.notifyItemChanged(position) - return@SwipeCallback - } - activity.deleteCookFoodAndGoods(foodId = list[position].foodId ?: "") { - dishAdapter.removeAt(position) - activity.toast("已删除") + // 点击列表空白区域时关闭已展开的侧滑 item + addOnItemTouchListener(object : androidx.recyclerview.widget.RecyclerView.SimpleOnItemTouchListener() { + override fun onInterceptTouchEvent(rv: androidx.recyclerview.widget.RecyclerView, e: MotionEvent): Boolean { + if (e.action == MotionEvent.ACTION_DOWN) { + dishAdapter.closeAll() + } + return false } }) - itemTouchHelper.attachToRecyclerView(this) } addViewListener() initObserver() diff --git a/app/src/main/java/com/shuwei/dish/match/view/swipereveallayout/SwipeRevealLayout.java b/app/src/main/java/com/shuwei/dish/match/view/swipereveallayout/SwipeRevealLayout.java new file mode 100644 index 0000000..1a24879 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/view/swipereveallayout/SwipeRevealLayout.java @@ -0,0 +1,1133 @@ +/** + The MIT License (MIT) + + Copyright (c) 2016 Chau Thai + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +package com.shuwei.dish.match.view.swipereveallayout; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Rect; +import androidx.core.view.GestureDetectorCompat; +import androidx.core.view.ViewCompat; +import androidx.customview.widget.ViewDragHelper; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.shuwei.dish.match.R; + +@SuppressLint("RtlHardcoded") +public class SwipeRevealLayout extends ViewGroup { + // These states are used only for ViewBindHelper + protected static final int STATE_CLOSE = 0; + protected static final int STATE_CLOSING = 1; + protected static final int STATE_OPEN = 2; + protected static final int STATE_OPENING = 3; + protected static final int STATE_DRAGGING = 4; + + private static final int DEFAULT_MIN_FLING_VELOCITY = 300; // dp per second + private static final int DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT = 1; // dp + + public static final int DRAG_EDGE_LEFT = 0x1; + public static final int DRAG_EDGE_RIGHT = 0x1 << 1; + public static final int DRAG_EDGE_TOP = 0x1 << 2; + public static final int DRAG_EDGE_BOTTOM = 0x1 << 3; + + /** + * The secondary view will be under the main view. + */ + public static final int MODE_NORMAL = 0; + + /** + * The secondary view will stick the edge of the main view. + */ + public static final int MODE_SAME_LEVEL = 1; + + /** + * Main view is the view which is shown when the layout is closed. + */ + private View mMainView; + + /** + * Secondary view is the view which is shown when the layout is opened. + */ + private View mSecondaryView; + + /** + * The rectangle position of the main view when the layout is closed. + */ + private Rect mRectMainClose = new Rect(); + + /** + * The rectangle position of the main view when the layout is opened. + */ + private Rect mRectMainOpen = new Rect(); + + /** + * The rectangle position of the secondary view when the layout is closed. + */ + private Rect mRectSecClose = new Rect(); + + /** + * The rectangle position of the secondary view when the layout is opened. + */ + private Rect mRectSecOpen = new Rect(); + + /** + * The minimum distance (px) to the closest drag edge that the SwipeRevealLayout + * will disallow the parent to intercept touch event. + */ + private int mMinDistRequestDisallowParent = 0; + + private boolean mIsOpenBeforeInit = false; + private volatile boolean mAborted = false; + private volatile boolean mIsScrolling = false; + private volatile boolean mLockDrag = false; + + private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY; + private int mState = STATE_CLOSE; + private int mMode = MODE_NORMAL; + + private int mLastMainLeft = 0; + private int mLastMainTop = 0; + + private int mDragEdge = DRAG_EDGE_LEFT; + + private float mDragDist = 0; + private float mPrevX = -1; + private float mPrevY = -1; + + private ViewDragHelper mDragHelper; + private GestureDetectorCompat mGestureDetector; + + private DragStateChangeListener mDragStateChangeListener; // only used for ViewBindHelper + private SwipeListener mSwipeListener; + + private int mOnLayoutCount = 0; + + interface DragStateChangeListener { + void onDragStateChanged(int state); + } + + /** + * Listener for monitoring events about swipe layout. + */ + public interface SwipeListener { + /** + * Called when the main view becomes completely closed. + */ + void onClosed(SwipeRevealLayout view); + + /** + * Called when the main view becomes completely opened. + */ + void onOpened(SwipeRevealLayout view); + + /** + * Called when the main view's position changes. + * @param slideOffset The new offset of the main view within its range, from 0-1 + */ + void onSlide(SwipeRevealLayout view, float slideOffset); + } + + /** + * No-op stub for {@link SwipeListener}. If you only want ot implement a subset + * of the listener methods, you can extend this instead of implement the full interface. + */ + public static class SimpleSwipeListener implements SwipeListener { + @Override + public void onClosed(SwipeRevealLayout view) {} + + @Override + public void onOpened(SwipeRevealLayout view) {} + + @Override + public void onSlide(SwipeRevealLayout view, float slideOffset) {} + } + + public SwipeRevealLayout(Context context) { + super(context); + init(context, null); + } + + public SwipeRevealLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public SwipeRevealLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mDragHelper.processTouchEvent(event); + // 未发生拖动时不消费事件,让子 View 的点击监听正常触发 + return mIsScrolling || mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (isDragLocked()) { + return super.onInterceptTouchEvent(ev); + } + + mDragHelper.processTouchEvent(ev); + mGestureDetector.onTouchEvent(ev); + accumulateDragDist(ev); + + boolean couldBecomeClick = couldBecomeClick(ev); + boolean settling = mDragHelper.getViewDragState() == ViewDragHelper.STATE_SETTLING; + boolean idleAfterScrolled = mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE + && mIsScrolling; + + // must be placed as the last statement + mPrevX = ev.getX(); + mPrevY = ev.getY(); + + // return true => intercept, cannot trigger onClick event + return !couldBecomeClick && (settling || idleAfterScrolled); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // get views + if (getChildCount() >= 2) { + mSecondaryView = getChildAt(0); + mMainView = getChildAt(1); + } + else if (getChildCount() == 1) { + mMainView = getChildAt(0); + } + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("ConstantConditions") + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mAborted = false; + + for (int index = 0; index < getChildCount(); index++) { + final View child = getChildAt(index); + + int left, right, top, bottom; + left = right = top = bottom = 0; + + final int minLeft = getPaddingLeft(); + final int maxRight = Math.max(r - getPaddingRight() - l, 0); + final int minTop = getPaddingTop(); + final int maxBottom = Math.max(b - getPaddingBottom() - t, 0); + + int measuredChildHeight = child.getMeasuredHeight(); + int measuredChildWidth = child.getMeasuredWidth(); + + // need to take account if child size is match_parent + final LayoutParams childParams = child.getLayoutParams(); + boolean matchParentHeight = false; + boolean matchParentWidth = false; + + if (childParams != null) { + matchParentHeight = (childParams.height == LayoutParams.MATCH_PARENT) || + (childParams.height == LayoutParams.FILL_PARENT); + matchParentWidth = (childParams.width == LayoutParams.MATCH_PARENT) || + (childParams.width == LayoutParams.FILL_PARENT); + } + + if (matchParentHeight) { + measuredChildHeight = maxBottom - minTop; + childParams.height = measuredChildHeight; + } + + if (matchParentWidth) { + measuredChildWidth = maxRight - minLeft; + childParams.width = measuredChildWidth; + } + + switch (mDragEdge) { + case DRAG_EDGE_RIGHT: + left = Math.max(r - measuredChildWidth - getPaddingRight() - l, minLeft); + top = Math.min(getPaddingTop(), maxBottom); + right = Math.max(r - getPaddingRight() - l, minLeft); + bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom); + break; + + case DRAG_EDGE_LEFT: + left = Math.min(getPaddingLeft(), maxRight); + top = Math.min(getPaddingTop(), maxBottom); + right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight); + bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom); + break; + + case DRAG_EDGE_TOP: + left = Math.min(getPaddingLeft(), maxRight); + top = Math.min(getPaddingTop(), maxBottom); + right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight); + bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom); + break; + + case DRAG_EDGE_BOTTOM: + left = Math.min(getPaddingLeft(), maxRight); + top = Math.max(b - measuredChildHeight - getPaddingBottom() - t, minTop); + right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight); + bottom = Math.max(b - getPaddingBottom() - t, minTop); + break; + } + + child.layout(left, top, right, bottom); + } + + // taking account offset when mode is SAME_LEVEL + if (mMode == MODE_SAME_LEVEL) { + switch (mDragEdge) { + case DRAG_EDGE_LEFT: + mSecondaryView.offsetLeftAndRight(-mSecondaryView.getWidth()); + break; + + case DRAG_EDGE_RIGHT: + mSecondaryView.offsetLeftAndRight(mSecondaryView.getWidth()); + break; + + case DRAG_EDGE_TOP: + mSecondaryView.offsetTopAndBottom(-mSecondaryView.getHeight()); + break; + + case DRAG_EDGE_BOTTOM: + mSecondaryView.offsetTopAndBottom(mSecondaryView.getHeight()); + } + } + + initRects(); + + if (mIsOpenBeforeInit) { + open(false); + } else { + close(false); + } + + mLastMainLeft = mMainView.getLeft(); + mLastMainTop = mMainView.getTop(); + + mOnLayoutCount++; + } + + /** + * {@inheritDoc} + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (getChildCount() < 2) { + throw new RuntimeException("Layout must have two children"); + } + + final LayoutParams params = getLayoutParams(); + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int desiredWidth = 0; + int desiredHeight = 0; + + // first find the largest child + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + measureChild(child, widthMeasureSpec, heightMeasureSpec); + desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth); + desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight); + } + // create new measure spec using the largest child width + widthMeasureSpec = MeasureSpec.makeMeasureSpec(desiredWidth, widthMode); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(desiredHeight, heightMode); + + final int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + final int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); + + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams childParams = child.getLayoutParams(); + + if (childParams != null) { + if (childParams.height == LayoutParams.MATCH_PARENT) { + child.setMinimumHeight(measuredHeight); + } + + if (childParams.width == LayoutParams.MATCH_PARENT) { + child.setMinimumWidth(measuredWidth); + } + } + + measureChild(child, widthMeasureSpec, heightMeasureSpec); + desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth); + desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight); + } + + // taking accounts of padding + desiredWidth += getPaddingLeft() + getPaddingRight(); + desiredHeight += getPaddingTop() + getPaddingBottom(); + + // adjust desired width + if (widthMode == MeasureSpec.EXACTLY) { + desiredWidth = measuredWidth; + } else { + if (params.width == LayoutParams.MATCH_PARENT) { + desiredWidth = measuredWidth; + } + + if (widthMode == MeasureSpec.AT_MOST) { + desiredWidth = (desiredWidth > measuredWidth)? measuredWidth : desiredWidth; + } + } + + // adjust desired height + if (heightMode == MeasureSpec.EXACTLY) { + desiredHeight = measuredHeight; + } else { + if (params.height == LayoutParams.MATCH_PARENT) { + desiredHeight = measuredHeight; + } + + if (heightMode == MeasureSpec.AT_MOST) { + desiredHeight = (desiredHeight > measuredHeight)? measuredHeight : desiredHeight; + } + } + + setMeasuredDimension(desiredWidth, desiredHeight); + } + + @Override + public void computeScroll() { + if (mDragHelper.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + /** + * Open the panel to show the secondary view + * @param animation true to animate the open motion. {@link SwipeListener} won't be + * called if is animation is false. + */ + public void open(boolean animation) { + mIsOpenBeforeInit = true; + mAborted = false; + + if (animation) { + mState = STATE_OPENING; + mDragHelper.smoothSlideViewTo(mMainView, mRectMainOpen.left, mRectMainOpen.top); + + if (mDragStateChangeListener != null) { + mDragStateChangeListener.onDragStateChanged(mState); + } + } else { + mState = STATE_OPEN; + mDragHelper.abort(); + + mMainView.layout( + mRectMainOpen.left, + mRectMainOpen.top, + mRectMainOpen.right, + mRectMainOpen.bottom + ); + + mSecondaryView.layout( + mRectSecOpen.left, + mRectSecOpen.top, + mRectSecOpen.right, + mRectSecOpen.bottom + ); + } + + ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this); + } + + /** + * Close the panel to hide the secondary view + * @param animation true to animate the close motion. {@link SwipeListener} won't be + * called if is animation is false. + */ + public void close(boolean animation) { + mIsOpenBeforeInit = false; + mAborted = false; + + if (animation) { + mState = STATE_CLOSING; + mDragHelper.smoothSlideViewTo(mMainView, mRectMainClose.left, mRectMainClose.top); + + if (mDragStateChangeListener != null) { + mDragStateChangeListener.onDragStateChanged(mState); + } + + } else { + mState = STATE_CLOSE; + mDragHelper.abort(); + + mMainView.layout( + mRectMainClose.left, + mRectMainClose.top, + mRectMainClose.right, + mRectMainClose.bottom + ); + + mSecondaryView.layout( + mRectSecClose.left, + mRectSecClose.top, + mRectSecClose.right, + mRectSecClose.bottom + ); + } + + ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this); + } + + /** + * Set the minimum fling velocity to cause the layout to open/close. + * @param velocity dp per second + */ + public void setMinFlingVelocity(int velocity) { + mMinFlingVelocity = velocity; + } + + /** + * Get the minimum fling velocity to cause the layout to open/close. + * @return dp per second + */ + public int getMinFlingVelocity() { + return mMinFlingVelocity; + } + + /** + * Set the edge where the layout can be dragged from. + * @param dragEdge Can be one of these + *
    + *
  • {@link #DRAG_EDGE_LEFT}
  • + *
  • {@link #DRAG_EDGE_TOP}
  • + *
  • {@link #DRAG_EDGE_RIGHT}
  • + *
  • {@link #DRAG_EDGE_BOTTOM}
  • + *
+ */ + public void setDragEdge(int dragEdge) { + mDragEdge = dragEdge; + } + + /** + * Get the edge where the layout can be dragged from. + * @return Can be one of these + *
    + *
  • {@link #DRAG_EDGE_LEFT}
  • + *
  • {@link #DRAG_EDGE_TOP}
  • + *
  • {@link #DRAG_EDGE_RIGHT}
  • + *
  • {@link #DRAG_EDGE_BOTTOM}
  • + *
+ */ + public int getDragEdge() { + return mDragEdge; + } + + public void setSwipeListener(SwipeListener listener) { + mSwipeListener = listener; + } + + /** + * @param lock if set to true, the user cannot drag/swipe the layout. + */ + public void setLockDrag(boolean lock) { + mLockDrag = lock; + } + + /** + * @return true if the drag/swipe motion is currently locked. + */ + public boolean isDragLocked() { + return mLockDrag; + } + + /** + * @return true if layout is fully opened, false otherwise. + */ + public boolean isOpened() { + return (mState == STATE_OPEN); + } + + /** + * @return true if layout is fully closed, false otherwise. + */ + public boolean isClosed() { + return (mState == STATE_CLOSE); + } + + /** Only used for {@link ViewBinderHelper} */ + void setDragStateChangeListener(DragStateChangeListener listener) { + mDragStateChangeListener = listener; + } + + /** Abort current motion in progress. Only used for {@link ViewBinderHelper} */ + protected void abort() { + mAborted = true; + mDragHelper.abort(); + } + + /** + * In RecyclerView/ListView, onLayout should be called 2 times to display children views correctly. + * This method check if it've already called onLayout two times. + * @return true if you should call {@link #requestLayout()}. + */ + protected boolean shouldRequestLayout() { + return mOnLayoutCount < 2; + } + + + private int getMainOpenLeft() { + switch (mDragEdge) { + case DRAG_EDGE_LEFT: + return mRectMainClose.left + mSecondaryView.getWidth(); + + case DRAG_EDGE_RIGHT: + return mRectMainClose.left - mSecondaryView.getWidth(); + + case DRAG_EDGE_TOP: + return mRectMainClose.left; + + case DRAG_EDGE_BOTTOM: + return mRectMainClose.left; + + default: + return 0; + } + } + + private int getMainOpenTop() { + switch (mDragEdge) { + case DRAG_EDGE_LEFT: + return mRectMainClose.top; + + case DRAG_EDGE_RIGHT: + return mRectMainClose.top; + + case DRAG_EDGE_TOP: + return mRectMainClose.top + mSecondaryView.getHeight(); + + case DRAG_EDGE_BOTTOM: + return mRectMainClose.top - mSecondaryView.getHeight(); + + default: + return 0; + } + } + + private int getSecOpenLeft() { + if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_BOTTOM || mDragEdge == DRAG_EDGE_TOP) { + return mRectSecClose.left; + } + + if (mDragEdge == DRAG_EDGE_LEFT) { + return mRectSecClose.left + mSecondaryView.getWidth(); + } else { + return mRectSecClose.left - mSecondaryView.getWidth(); + } + } + + private int getSecOpenTop() { + if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) { + return mRectSecClose.top; + } + + if (mDragEdge == DRAG_EDGE_TOP) { + return mRectSecClose.top + mSecondaryView.getHeight(); + } else { + return mRectSecClose.top - mSecondaryView.getHeight(); + } + } + + private void initRects() { + // close position of main view + mRectMainClose.set( + mMainView.getLeft(), + mMainView.getTop(), + mMainView.getRight(), + mMainView.getBottom() + ); + + // close position of secondary view + mRectSecClose.set( + mSecondaryView.getLeft(), + mSecondaryView.getTop(), + mSecondaryView.getRight(), + mSecondaryView.getBottom() + ); + + // open position of the main view + mRectMainOpen.set( + getMainOpenLeft(), + getMainOpenTop(), + getMainOpenLeft() + mMainView.getWidth(), + getMainOpenTop() + mMainView.getHeight() + ); + + // open position of the secondary view + mRectSecOpen.set( + getSecOpenLeft(), + getSecOpenTop(), + getSecOpenLeft() + mSecondaryView.getWidth(), + getSecOpenTop() + mSecondaryView.getHeight() + ); + } + + private boolean couldBecomeClick(MotionEvent ev) { + return isInMainView(ev) && !shouldInitiateADrag(); + } + + private boolean isInMainView(MotionEvent ev) { + float x = ev.getX(); + float y = ev.getY(); + + boolean withinVertical = mMainView.getTop() <= y && y <= mMainView.getBottom(); + boolean withinHorizontal = mMainView.getLeft() <= x && x <= mMainView.getRight(); + + return withinVertical && withinHorizontal; + } + + private boolean shouldInitiateADrag() { + float minDistToInitiateDrag = mDragHelper.getTouchSlop(); + return mDragDist >= minDistToInitiateDrag; + } + + private void accumulateDragDist(MotionEvent ev) { + final int action = ev.getAction(); + if (action == MotionEvent.ACTION_DOWN) { + mDragDist = 0; + return; + } + + boolean dragHorizontally = getDragEdge() == DRAG_EDGE_LEFT || + getDragEdge() == DRAG_EDGE_RIGHT; + + float dragged; + if (dragHorizontally) { + dragged = Math.abs(ev.getX() - mPrevX); + } else { + dragged = Math.abs(ev.getY() - mPrevY); + } + + mDragDist += dragged; + } + + private void init(Context context, AttributeSet attrs) { + if (attrs != null && context != null) { + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.SwipeRevealLayout, + 0, 0 + ); + + mDragEdge = a.getInteger(R.styleable.SwipeRevealLayout_dragEdge, DRAG_EDGE_LEFT); + mMinFlingVelocity = a.getInteger(R.styleable.SwipeRevealLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY); + mMode = a.getInteger(R.styleable.SwipeRevealLayout_mode, MODE_NORMAL); + + mMinDistRequestDisallowParent = a.getDimensionPixelSize( + R.styleable.SwipeRevealLayout_minDistRequestDisallowParent, + dpToPx(DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT) + ); + } + + mDragHelper = ViewDragHelper.create(this, 1.0f, mDragHelperCallback); + mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL); + + mGestureDetector = new GestureDetectorCompat(context, mGestureListener); + } + + private final GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { + boolean hasDisallowed = false; + + @Override + public boolean onDown(MotionEvent e) { + mIsScrolling = false; + hasDisallowed = false; + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + mIsScrolling = true; + return false; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + mIsScrolling = true; + + if (getParent() != null) { + // 判断滑动方向:水平分量大于垂直分量时才阻止父布局拦截 + // 避免垂直滑动时误触发 SmartRefreshLayout 的下拉刷新 + boolean isHorizontalScroll = Math.abs(distanceX) >= Math.abs(distanceY); + + boolean shouldDisallow; + if (!isHorizontalScroll) { + // 垂直滑动,允许父布局(SmartRefreshLayout/RecyclerView)正常处理 + shouldDisallow = false; + } else if (!hasDisallowed) { + shouldDisallow = getDistToClosestEdge() >= mMinDistRequestDisallowParent; + if (shouldDisallow) { + hasDisallowed = true; + } + } else { + shouldDisallow = true; + } + + // 阻止父布局拦截触摸事件,确保水平滑动正常工作 + getParent().requestDisallowInterceptTouchEvent(shouldDisallow); + } + + return false; + } + }; + + private int getDistToClosestEdge() { + switch (mDragEdge) { + case DRAG_EDGE_LEFT: + final int pivotRight = mRectMainClose.left + mSecondaryView.getWidth(); + + return Math.min( + mMainView.getLeft() - mRectMainClose.left, + pivotRight - mMainView.getLeft() + ); + + case DRAG_EDGE_RIGHT: + final int pivotLeft = mRectMainClose.right - mSecondaryView.getWidth(); + + return Math.min( + mMainView.getRight() - pivotLeft, + mRectMainClose.right - mMainView.getRight() + ); + + case DRAG_EDGE_TOP: + final int pivotBottom = mRectMainClose.top + mSecondaryView.getHeight(); + + return Math.min( + mMainView.getBottom() - pivotBottom, + pivotBottom - mMainView.getTop() + ); + + case DRAG_EDGE_BOTTOM: + final int pivotTop = mRectMainClose.bottom - mSecondaryView.getHeight(); + + return Math.min( + mRectMainClose.bottom - mMainView.getBottom(), + mMainView.getBottom() - pivotTop + ); + } + + return 0; + } + + private int getHalfwayPivotHorizontal() { + if (mDragEdge == DRAG_EDGE_LEFT) { + return mRectMainClose.left + mSecondaryView.getWidth() / 2; + } else { + return mRectMainClose.right - mSecondaryView.getWidth() / 2; + } + } + + private int getHalfwayPivotVertical() { + if (mDragEdge == DRAG_EDGE_TOP) { + return mRectMainClose.top + mSecondaryView.getHeight() / 2; + } else { + return mRectMainClose.bottom - mSecondaryView.getHeight() / 2; + } + } + + private final ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() { + @Override + public boolean tryCaptureView(View child, int pointerId) { + mAborted = false; + + if (mLockDrag) + return false; + + mDragHelper.captureChildView(mMainView, pointerId); + return false; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + switch (mDragEdge) { + case DRAG_EDGE_TOP: + return Math.max( + Math.min(top, mRectMainClose.top + mSecondaryView.getHeight()), + mRectMainClose.top + ); + + case DRAG_EDGE_BOTTOM: + return Math.max( + Math.min(top, mRectMainClose.top), + mRectMainClose.top - mSecondaryView.getHeight() + ); + + default: + return child.getTop(); + } + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + switch (mDragEdge) { + case DRAG_EDGE_RIGHT: + return Math.max( + Math.min(left, mRectMainClose.left), + mRectMainClose.left - mSecondaryView.getWidth() + ); + + case DRAG_EDGE_LEFT: + return Math.max( + Math.min(left, mRectMainClose.left + mSecondaryView.getWidth()), + mRectMainClose.left + ); + + default: + return child.getLeft(); + } + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + final boolean velRightExceeded = pxToDp((int) xvel) >= mMinFlingVelocity; + final boolean velLeftExceeded = pxToDp((int) xvel) <= -mMinFlingVelocity; + final boolean velUpExceeded = pxToDp((int) yvel) <= -mMinFlingVelocity; + final boolean velDownExceeded = pxToDp((int) yvel) >= mMinFlingVelocity; + + final int pivotHorizontal = getHalfwayPivotHorizontal(); + final int pivotVertical = getHalfwayPivotVertical(); + + switch (mDragEdge) { + case DRAG_EDGE_RIGHT: + if (velRightExceeded) { + close(true); + } else if (velLeftExceeded) { + open(true); + } else { + if (mMainView.getRight() < pivotHorizontal) { + open(true); + } else { + close(true); + } + } + break; + + case DRAG_EDGE_LEFT: + if (velRightExceeded) { + open(true); + } else if (velLeftExceeded) { + close(true); + } else { + if (mMainView.getLeft() < pivotHorizontal) { + close(true); + } else { + open(true); + } + } + break; + + case DRAG_EDGE_TOP: + if (velUpExceeded) { + close(true); + } else if (velDownExceeded) { + open(true); + } else { + if (mMainView.getTop() < pivotVertical) { + close(true); + } else { + open(true); + } + } + break; + + case DRAG_EDGE_BOTTOM: + if (velUpExceeded) { + open(true); + } else if (velDownExceeded) { + close(true); + } else { + if (mMainView.getBottom() < pivotVertical) { + open(true); + } else { + close(true); + } + } + break; + } + } + + @Override + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + super.onEdgeDragStarted(edgeFlags, pointerId); + + if (mLockDrag) { + return; + } + + boolean edgeStartLeft = (mDragEdge == DRAG_EDGE_RIGHT) + && edgeFlags == ViewDragHelper.EDGE_LEFT; + + boolean edgeStartRight = (mDragEdge == DRAG_EDGE_LEFT) + && edgeFlags == ViewDragHelper.EDGE_RIGHT; + + boolean edgeStartTop = (mDragEdge == DRAG_EDGE_BOTTOM) + && edgeFlags == ViewDragHelper.EDGE_TOP; + + boolean edgeStartBottom = (mDragEdge == DRAG_EDGE_TOP) + && edgeFlags == ViewDragHelper.EDGE_BOTTOM; + + if (edgeStartLeft || edgeStartRight || edgeStartTop || edgeStartBottom) { + mDragHelper.captureChildView(mMainView, pointerId); + } + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + super.onViewPositionChanged(changedView, left, top, dx, dy); + if (mMode == MODE_SAME_LEVEL) { + if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) { + mSecondaryView.offsetLeftAndRight(dx); + } else { + mSecondaryView.offsetTopAndBottom(dy); + } + } + + boolean isMoved = (mMainView.getLeft() != mLastMainLeft) || (mMainView.getTop() != mLastMainTop); + if (mSwipeListener != null && isMoved) { + if (mMainView.getLeft() == mRectMainClose.left && mMainView.getTop() == mRectMainClose.top) { + mSwipeListener.onClosed(SwipeRevealLayout.this); + } + else if (mMainView.getLeft() == mRectMainOpen.left && mMainView.getTop() == mRectMainOpen.top) { + mSwipeListener.onOpened(SwipeRevealLayout.this); + } + else { + mSwipeListener.onSlide(SwipeRevealLayout.this, getSlideOffset()); + } + } + + mLastMainLeft = mMainView.getLeft(); + mLastMainTop = mMainView.getTop(); + ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this); + } + + private float getSlideOffset() { + switch (mDragEdge) { + case DRAG_EDGE_LEFT: + return (float) (mMainView.getLeft() - mRectMainClose.left) / mSecondaryView.getWidth(); + + case DRAG_EDGE_RIGHT: + return (float) (mRectMainClose.left - mMainView.getLeft()) / mSecondaryView.getWidth(); + + case DRAG_EDGE_TOP: + return (float) (mMainView.getTop() - mRectMainClose.top) / mSecondaryView.getHeight(); + + case DRAG_EDGE_BOTTOM: + return (float) (mRectMainClose.top - mMainView.getTop()) / mSecondaryView.getHeight(); + + default: + return 0; + } + } + + @Override + public void onViewDragStateChanged(int state) { + super.onViewDragStateChanged(state); + final int prevState = mState; + + switch (state) { + case ViewDragHelper.STATE_DRAGGING: + mState = STATE_DRAGGING; + break; + + case ViewDragHelper.STATE_IDLE: + + // drag edge is left or right + if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) { + if (mMainView.getLeft() == mRectMainClose.left) { + mState = STATE_CLOSE; + } else { + mState = STATE_OPEN; + } + } + + // drag edge is top or bottom + else { + if (mMainView.getTop() == mRectMainClose.top) { + mState = STATE_CLOSE; + } else { + mState = STATE_OPEN; + } + } + break; + } + + if (mDragStateChangeListener != null && !mAborted && prevState != mState) { + mDragStateChangeListener.onDragStateChanged(mState); + } + } + }; + + public static String getStateString(int state) { + switch (state) { + case STATE_CLOSE: + return "state_close"; + + case STATE_CLOSING: + return "state_closing"; + + case STATE_OPEN: + return "state_open"; + + case STATE_OPENING: + return "state_opening"; + + case STATE_DRAGGING: + return "state_dragging"; + + default: + return "undefined"; + } + } + + private int pxToDp(int px) { + Resources resources = getContext().getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return (int) (px / ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)); + } + + private int dpToPx(int dp) { + Resources resources = getContext().getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)); + } +} diff --git a/app/src/main/java/com/shuwei/dish/match/view/swipereveallayout/ViewBinderHelper.java b/app/src/main/java/com/shuwei/dish/match/view/swipereveallayout/ViewBinderHelper.java new file mode 100644 index 0000000..9ed5f5e --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/view/swipereveallayout/ViewBinderHelper.java @@ -0,0 +1,271 @@ +/** + The MIT License (MIT) + + Copyright (c) 2016 Chau Thai + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +package com.shuwei.dish.match.view.swipereveallayout; + +import android.os.Bundle; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * ViewBinderHelper provides a quick and easy solution to restore the open/close state + * of the items in RecyclerView, ListView, GridView or any view that requires its child view + * to bind the view to a data object. + * + *

When you bind you data object to a view, use {@link #bind(SwipeRevealLayout, String)} to + * save and restore the open/close state of the view.

+ * + *

Optionally, if you also want to save and restore the open/close state when the device's + * orientation is changed, call {@link #saveStates(Bundle)} in {@link android.app.Activity#onSaveInstanceState(Bundle)} + * and {@link #restoreStates(Bundle)} in {@link android.app.Activity#onRestoreInstanceState(Bundle)}

+ */ +public class ViewBinderHelper { + private static final String BUNDLE_MAP_KEY = "ViewBinderHelper_Bundle_Map_Key"; + + private Map mapStates = Collections.synchronizedMap(new HashMap()); + private Map mapLayouts = Collections.synchronizedMap(new HashMap()); + private Set lockedSwipeSet = Collections.synchronizedSet(new HashSet()); + + private volatile boolean openOnlyOne = false; + private final Object stateChangeLock = new Object(); + + /** + * Help to save and restore open/close state of the swipeLayout. Call this method + * when you bind your view holder with the data object. + * + * @param swipeLayout swipeLayout of the current view. + * @param id a string that uniquely defines the data object of the current view. + */ + public void bind(final SwipeRevealLayout swipeLayout, final String id) { + if (swipeLayout.shouldRequestLayout()) { + swipeLayout.requestLayout(); + } + + mapLayouts.values().remove(swipeLayout); + mapLayouts.put(id, swipeLayout); + + swipeLayout.abort(); + swipeLayout.setDragStateChangeListener(new SwipeRevealLayout.DragStateChangeListener() { + @Override + public void onDragStateChanged(int state) { + mapStates.put(id, state); + + if (openOnlyOne) { + closeOthers(id, swipeLayout); + } + } + }); + + // first time binding. + if (!mapStates.containsKey(id)) { + mapStates.put(id, SwipeRevealLayout.STATE_CLOSE); + swipeLayout.close(false); + } + + // not the first time, then close or open depends on the current state. + else { + int state = mapStates.get(id); + + if (state == SwipeRevealLayout.STATE_CLOSE || state == SwipeRevealLayout.STATE_CLOSING || + state == SwipeRevealLayout.STATE_DRAGGING) { + swipeLayout.close(false); + } else { + swipeLayout.open(false); + } + } + + // set lock swipe + swipeLayout.setLockDrag(lockedSwipeSet.contains(id)); + } + + /** + * Only if you need to restore open/close state when the orientation is changed. + * Call this method in {@link android.app.Activity#onSaveInstanceState(Bundle)} + */ + public void saveStates(Bundle outState) { + if (outState == null) + return; + + Bundle statesBundle = new Bundle(); + for (Map.Entry entry : mapStates.entrySet()) { + statesBundle.putInt(entry.getKey(), entry.getValue()); + } + + outState.putBundle(BUNDLE_MAP_KEY, statesBundle); + } + + + /** + * Only if you need to restore open/close state when the orientation is changed. + * Call this method in {@link android.app.Activity#onRestoreInstanceState(Bundle)} + */ + @SuppressWarnings({"unchecked", "ConstantConditions"}) + public void restoreStates(Bundle inState) { + if (inState == null) + return; + + if (inState.containsKey(BUNDLE_MAP_KEY)) { + HashMap restoredMap = new HashMap<>(); + + Bundle statesBundle = inState.getBundle(BUNDLE_MAP_KEY); + Set keySet = statesBundle.keySet(); + + if (keySet != null) { + for (String key : keySet) { + restoredMap.put(key, statesBundle.getInt(key)); + } + } + + mapStates = restoredMap; + } + } + + /** + * Lock swipe for some layouts. + * @param id a string that uniquely defines the data object. + */ + public void lockSwipe(String... id) { + setLockSwipe(true, id); + } + + /** + * Unlock swipe for some layouts. + * @param id a string that uniquely defines the data object. + */ + public void unlockSwipe(String... id) { + setLockSwipe(false, id); + } + + /** + * @param openOnlyOne If set to true, then only one row can be opened at a time. + */ + public void setOpenOnlyOne(boolean openOnlyOne) { + this.openOnlyOne = openOnlyOne; + } + + /** + * Open a specific layout. + * @param id unique id which identifies the data object which is bind to the layout. + */ + public void openLayout(final String id) { + synchronized (stateChangeLock) { + mapStates.put(id, SwipeRevealLayout.STATE_OPEN); + + if (mapLayouts.containsKey(id)) { + final SwipeRevealLayout layout = mapLayouts.get(id); + layout.open(true); + } + else if (openOnlyOne) { + closeOthers(id, mapLayouts.get(id)); + } + } + } + + /** + * Close a specific layout. + * @param id unique id which identifies the data object which is bind to the layout. + */ + public void closeLayout(final String id) { + synchronized (stateChangeLock) { + mapStates.put(id, SwipeRevealLayout.STATE_CLOSE); + + if (mapLayouts.containsKey(id)) { + final SwipeRevealLayout layout = mapLayouts.get(id); + layout.close(true); + } + } + } + + /** + * 关闭所有已展开的侧滑布局,用于点击空白区域时收起侧滑。 + */ + public void closeAll() { + synchronized (stateChangeLock) { + for (Map.Entry entry : mapStates.entrySet()) { + entry.setValue(SwipeRevealLayout.STATE_CLOSE); + } + for (SwipeRevealLayout layout : mapLayouts.values()) { + layout.close(true); + } + } + } + + /** + * Close others swipe layout. + * @param id layout which bind with this data object id will be excluded. + * @param swipeLayout will be excluded. + */ + private void closeOthers(String id, SwipeRevealLayout swipeLayout) { + synchronized (stateChangeLock) { + // close other rows if openOnlyOne is true. + if (getOpenCount() > 1) { + for (Map.Entry entry : mapStates.entrySet()) { + if (!entry.getKey().equals(id)) { + entry.setValue(SwipeRevealLayout.STATE_CLOSE); + } + } + + for (SwipeRevealLayout layout : mapLayouts.values()) { + if (layout != swipeLayout) { + layout.close(true); + } + } + } + } + } + + private void setLockSwipe(boolean lock, String... id) { + if (id == null || id.length == 0) + return; + + if (lock) + lockedSwipeSet.addAll(Arrays.asList(id)); + else + lockedSwipeSet.removeAll(Arrays.asList(id)); + + for (String s : id) { + SwipeRevealLayout layout = mapLayouts.get(s); + if (layout != null) { + layout.setLockDrag(lock); + } + } + } + + private int getOpenCount() { + int total = 0; + + for (int state : mapStates.values()) { + if (state == SwipeRevealLayout.STATE_OPEN || state == SwipeRevealLayout.STATE_OPENING) { + total++; + } + } + + return total; + } +} diff --git a/app/src/main/res/drawable/bg_swipe_delete.xml b/app/src/main/res/drawable/bg_swipe_delete.xml new file mode 100644 index 0000000..6cd8eaa --- /dev/null +++ b/app/src/main/res/drawable/bg_swipe_delete.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_trash_white.xml b/app/src/main/res/drawable/ic_trash_white.xml index 9566771..e6d7c5d 100644 --- a/app/src/main/res/drawable/ic_trash_white.xml +++ b/app/src/main/res/drawable/ic_trash_white.xml @@ -1,32 +1,15 @@ - + - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="1024" + android:viewportHeight="1024"> - - + android:pathData="M913.5,225.8h-241V128c0,-34.5 -28.3,-62.8 -62.8,-62.8H413.3c-34.5,0 -62.8,28.3 -62.8,62.8v97.8H111.1c-25.8,0 -46.9,21.1 -46.9,46.9 0,25.8 21.1,46.9 46.9,46.9h81v590.3c0,25.8 21.1,46.9 46.9,46.9h546.4c25.8,0 46.9,-21.1 46.9,-46.9 0,-1.1 -0.1,-2.2 -0.1,-3.3V319.5h81.5c25.8,0 46.9,-21.1 46.9,-46.9 -0.2,-25.7 -21.3,-46.8 -47.1,-46.8zM448.1,156.1c0,-0.1 0,-0.1 0,0l128,-0.1s0.1,0 0.1,0.1v69.7h-128v-69.7zM737,863H287V319.5h450V863z" /> - + android:pathData="M420.8,767.8c27.4,0 49.8,-22.4 49.8,-49.8V466c0,-27.4 -22.4,-49.8 -49.8,-49.8S371,438.6 371,466v252c0,27.4 22.4,49.8 49.8,49.8zM602.9,767.8c27.4,0 49.8,-22.4 49.8,-49.8V466c0,-27.4 -22.4,-49.8 -49.8,-49.8s-49.8,22.4 -49.8,49.8v252c0,27.4 22.4,49.8 49.8,49.8z" /> + diff --git a/app/src/main/res/layout/list_item_dish_cook.xml b/app/src/main/res/layout/list_item_dish_cook.xml index 18a76ae..b6103fe 100644 --- a/app/src/main/res/layout/list_item_dish_cook.xml +++ b/app/src/main/res/layout/list_item_dish_cook.xml @@ -1,85 +1,109 @@ - + app:dragEdge="right"> - + - + - + - + - + - \ No newline at end of file + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_food_list.xml b/app/src/main/res/layout/list_item_food_list.xml index b784ed2..2b89b7a 100644 --- a/app/src/main/res/layout/list_item_food_list.xml +++ b/app/src/main/res/layout/list_item_food_list.xml @@ -1,61 +1,87 @@ - + app:dragEdge="right"> - + + - + - + - + + + + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..4fe87f3 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file