美文网首页Android
Android模仿实现Instagram照片选择页的效果

Android模仿实现Instagram照片选择页的效果

作者: 景山道人 | 来源:发表于2019-05-09 18:07 被阅读3次

    上次试着搞了搞点击回到顶部的效果,不过最后也没搞出个所以然来,这次是照片选择和上传页的效果,找到了一个别人的项目所以分享一下。

    先放Ins上的效果(强行调分辨率弄的图有点糊):


    展开

    布局:直观看过去就是外层的Toolbar和ViewPager我们先不管,再里面LinearLayout里装着ImageView + RecyclerView

    收起

    总结下来,简单来说实际上就是如果手只在RecyclerView的范围内划动就正常滑照片列表,划到上面的照片的话就把照片推上去
    其他一些别的效果回头再说。

    关于这个效果我找到了一个实现用的Demo,感谢大佬作者
    Github: InstagramPhotoPicker by Skykai521

    原版代码各位自己点进去看就是了,我改了改来实现点别的,以及debug
    我就不全贴了,贴一部分核心逻辑和能改的东西
    关于里面的逻辑全写在注释里了,应该已经写得很详细了
    使用方法和注意事项在最下面
    如果哪写错了欢迎和我说……

    /**
     * Created by sky on 17/3/1.
     * https://github.com/Skykai521/InstagramPhotoPicker
     */
    public class CoordinatorRecyclerView extends RecyclerView {
     
        ...
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent e) {
            // 我自己加的,原因是onTouchEvent的down这个event
            // 在RecyclerView的item是clickable的时候很容易失效,
            // 导致downPositionY不更新,会有bug,折叠上去之后拽不下来
            // 所以把down的处理也放在这里
            if (e.getAction() == MotionEvent.ACTION_DOWN) {
                downPositionY = e.getRawY();
            }
            return super.onInterceptTouchEvent(e);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (null == coordinatorListener) {
                return super.onTouchEvent(ev);
            }
            final int action = ev.getAction();
            final int y = (int) ev.getRawY();
            final int x = (int) ev.getRawX();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    downPositionY = ev.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int deltaY = (int) (downPositionY - y);
                    boolean deal;
                    if (isScrollTop(ev)) {
                        // 折叠着且recycler拉到头,大图被拽下来
                        deal = coordinatorListener.onCoordinateScroll(x, y, 0, deltaY + Math.abs(dragDistanceY), true);
                    } else {
                        // 大图展开
                        deal = coordinatorListener.onCoordinateScroll(x, y, 0, deltaY, isScrollTop(ev));
                    }
                    if (deal) {
                        // 这里手动调了下stopScroll,是因为每次大图收起来之后
                        // item的点击事件会有一次失效,推测是这次点击被用来停止滚动了,所以手动给他停下
                        stopScroll();
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    // 这里即松手判断大图位置是不是变了,变了就自动收起/折叠
                    scrollTop = false;
                    if (coordinatorListener.isBeingDragged()) {
                        coordinatorListener.onSwitch();
                        return true;
                    }
                    break;
            }
            return super.onTouchEvent(ev);
        }
    
        private boolean isScrollTop(MotionEvent ev) {
            // 在折叠状态下,RecyclerView依然是可以上下滚的,
            // 只有RecyclerView下拉到头马上要把上面折叠的大图拽下来了时是isScrollTop
            LayoutManager layoutManager = getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
                if (gridLayoutManager.findFirstVisibleItemPosition() == 0) {
                    ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) gridLayoutManager.findViewByPosition(0).getLayoutParams();
                    // 这里代表RecyclerView下拉时被拉到头了
                    // 一般情况下下面两个条件必定有一个为true,所以这里用&&
                    // 这里的逻辑我也修改过,大致意思是第一个图片toolbar底部的高度等于decoration或者margin
                    // 根据情况可以自己添加,因为这里出错会导致折叠的大图拉不下来
                    if ((null != params && gridLayoutManager.findViewByPosition(0).getTop() != params.topMargin) &&
                            gridLayoutManager.findViewByPosition(0).getTop() != gridLayoutManager.getTopDecorationHeight(gridLayoutManager.findViewByPosition(0))) {
                        return false;
                    }
                    if (!scrollTop) {
                        // 这里的dragDistanceY即大图折叠时RecyclerView被拽着滚动的距离
                        dragDistanceY = (int) (downPositionY - ev.getRawY());
                        scrollTop = true;
                    }
                    return true;
                }
            }
            return false;
        }
    
        public void setCoordinatorListener(CoordinatorListener listener) {
            this.coordinatorListener = listener;
        }
    
        @Override
        public void onScrolled(int dx, int dy) {
            // 原本接口类里没定义switchToTop和isWholeState这俩方法,
            // 所以想用listener调用得自己加上,作用是滚过一段距离之后自动展开
            super.onScrolled(dx, dy);
            totalY += dy;
            if ((totalY > onSwitchDistance || totalY < -onSwitchDistance) && coordinatorListener.isWholeState()) {
                coordinatorListener.switchToTop();
                totalY = 0;
            }
        }
    
        public void onItemClick(int position) {
            // 自己写的,搞这个是为了点击item时大图能展开,且item移动到大图正下面
            if (null == coordinatorListener) {
                return;
            }
            GridLayoutManager manager = (GridLayoutManager) getLayoutManager();
            int firstPosition = manager.findFirstVisibleItemPosition();
            int availablePosition = position - firstPosition;
            // 如果position大于屏幕中显示的child数量就会为空,所以这里要减去
            View child = getLayoutManager().getChildAt(availablePosition);
            if (null != child) {
                scrollBy(0, child.getTop());
            }
            if (!coordinatorListener.isWholeState()) {
                coordinatorListener.switchToWhole();
            }
            totalY = 0;
        }
    
    }
    
    /**
     * Created by sky on 17/3/1.
     * https://github.com/Skykai521/InstagramPhotoPicker
     */
    public class CoordinatorLinearLayout extends LinearLayout implements CoordinatorListener {
        public static int DEFAULT_DURATION = 500;
        private int state = WHOLE_STATE;
        private int topBarHeight; // toolbar
        private int topViewHeight; // toolbar + 正方形大照片的底部高度
        private int minScrollToTop; // toolbar
        private int minScrollToWhole; // 大照片高度 - toolbar,和上面的minScrollToTop一起,用于判断松手后展开还是收起
        private int maxScrollDistance; // 大照片高度,最大滑动距离
        private float lastPositionY; // 手指按下的位置
        private boolean beingDragged;
        private Context context;
        private OverScroller scroller; // 用于松手后展开/收起
    
        ...
    
        public CoordinatorLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            this.context = context;
            init();
        }
    
        private void init() {
            scroller = new OverScroller(context);
        }
    
        public void setTopViewParam(int topViewHeight, int topBarHeight) {
            // 初始化这些值,这些定义错了这个类是没法实现效果的
            this.topViewHeight = topViewHeight;
            this.topBarHeight = topBarHeight;
            this.maxScrollDistance = this.topViewHeight - this.topBarHeight;
            this.minScrollToTop = this.topBarHeight;
            this.minScrollToWhole = maxScrollDistance - this.topBarHeight;
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            final int action = ev.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    int y = (int) ev.getY();
                    int rawY = (int) ev.getRawY();
                    lastPositionY = y;
                    // 收起且点在最顶上,在这里处理,这里用getY和getRawY是会有区别的,看情况用吧
                    if (state == COLLAPSE_STATE && rawY < topBarHeight) {
                        return true;
                    }
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            // 应该是只有碰到最顶上了才会走到这里
            final int action = ev.getAction();
            final int y = (int) ev.getRawY();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    lastPositionY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    int deltaY = (int) (lastPositionY - y);
                    if (state == COLLAPSE_STATE && deltaY < 0) {
                        beingDragged = true;
                        setScrollY(maxScrollDistance + deltaY);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    if (beingDragged) {
                        onSwitch();
                        return true;
                    }
                    break;
            }
            return true;
        }
    
        @Override
        public boolean onCoordinateScroll(int x, int y, int deltaX, int deltaY, boolean isScrollToTop) {
            // deltaY 是按下位置 - 手指拖动后的位置
            if (y < topViewHeight && state == WHOLE_STATE && getScrollY() < getScrollRange()) {
                // 展开,手指在滑动区间(toolbar + 正方形)且在范围内(正方形高度)
                beingDragged = true;
                // 手指当前位置和开始滑动的位置的距离
                setScrollY(topViewHeight - y);
                return true;
            } else if (isScrollToTop && state == COLLAPSE_STATE && deltaY < 0) {
                // 在顶上,收起且向下滑
                beingDragged = true;
                setScrollY(maxScrollDistance + deltaY);
                return true;
            } else {
                return false;
            }
        }
    
        @Override
        public void onSwitch() {
            if (state == WHOLE_STATE) {
                if (getScrollY() >= minScrollToTop) {
                    switchToTop();
                } else {
                    switchToWhole();
                }
            } else if (state == COLLAPSE_STATE) {
                if (getScrollY() <= minScrollToWhole) {
                    switchToWhole();
                } else {
                    switchToTop();
                }
            }
        }
    
        @Override
        public boolean isBeingDragged() {
            return beingDragged;
        }
    
        public void switchToWhole() {
            if (!scroller.isFinished()) {
                scroller.abortAnimation();
            }
            // 滚到原来的位置
            scroller.startScroll(0, getScrollY(), 0, -getScrollY(), DEFAULT_DURATION);
            postInvalidate();
            state = WHOLE_STATE;
            beingDragged = false;
        }
    
        public void switchToTop() {
            if (!scroller.isFinished()) {
                scroller.abortAnimation();
            }
            scroller.startScroll(0, getScrollY(), 0, getScrollRange() - getScrollY(), DEFAULT_DURATION);
            postInvalidate();
            state = COLLAPSE_STATE;
            beingDragged = false;
        }
    
        @Override
        public void computeScroll() {
            // 重写这个来让LinearLayout可以滚动
            if (scroller.computeScrollOffset()) {
                setScrollY(scroller.getCurrY());
                postInvalidate();
            }
        }
    
        private int getScrollRange() {
            return maxScrollDistance;
        }
    
        @Override
        public boolean isWholeState() {
            return state == WHOLE_STATE;
        }
    }
    

    使用方法:
    分别find出对象,然后将CoordinatorLinearLayout调用setCoordinatorListener给CoordinatorRecyclerView就好了
    然后调用CoordinatorLinearLayout的setTopViewParam设置高度
    至于RecyclerView的设置manager和adapter啥的就不说了

    注意事项:
    设置高度不要出错,一个toolbar+大照片高度,一个toolbar高度
    记得也给RecyclerView重设下高度,不然它划上去也只有被啃剩下那点高度,(一些别的情况下高度设置可能会失效,这个我就不管了……Google吧)
    设置RV的高度时注意设置成它最大能展示在屏幕里的高度,设多了最后滚到最下面会显示不全

    可能的问题

    1. 折叠上去以后第一次点击失效:可能是RecyclerView的ScrollState没更新导致的
    2. 折叠上去之后拽不下来:downPositionY位置没更新导致的
    3. 别的我没发现的问题
      前两个在注释里有提到原因和解决办法

    相关文章

      网友评论

        本文标题:Android模仿实现Instagram照片选择页的效果

        本文链接:https://www.haomeiwen.com/subject/cfzvoqtx.html