Android

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

2019-05-09  本文已影响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. 别的我没发现的问题
    前两个在注释里有提到原因和解决办法
上一篇下一篇

猜你喜欢

热点阅读