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

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

先放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. 别的我没发现的问题
    前两个在注释里有提到原因和解决办法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,527评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,687评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,640评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,957评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,682评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,011评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,009评论 3 449
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,183评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,714评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,435评论 3 359
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,665评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,148评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,838评论 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,251评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,588评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,379评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,627评论 2 380

推荐阅读更多精彩内容