Android模仿實(shí)現(xiàn)Instagram照片選擇頁的效果

上次試著搞了搞點(diǎn)擊回到頂部的效果,不過最后也沒搞出個(gè)所以然來,這次是照片選擇和上傳頁的效果,找到了一個(gè)別人的項(xiàng)目所以分享一下。

先放Ins上的效果(強(qiáng)行調(diào)分辨率弄的圖有點(diǎn)糊):


展開

布局:直觀看過去就是外層的Toolbar和ViewPager我們先不管,再里面LinearLayout里裝著ImageView + RecyclerView

收起

總結(jié)下來,簡單來說實(shí)際上就是如果手只在RecyclerView的范圍內(nèi)劃動(dòng)就正?;掌斜?,劃到上面的照片的話就把照片推上去
其他一些別的效果回頭再說。

關(guān)于這個(gè)效果我找到了一個(gè)實(shí)現(xiàn)用的Demo,感謝大佬作者
Github: InstagramPhotoPicker by Skykai521

原版代碼各位自己點(diǎn)進(jìn)去看就是了,我改了改來實(shí)現(xiàn)點(diǎn)別的,以及debug
我就不全貼了,貼一部分核心邏輯和能改的東西
關(guān)于里面的邏輯全寫在注釋里了,應(yīng)該已經(jīng)寫得很詳細(xì)了
使用方法和注意事項(xiàng)在最下面
如果哪寫錯(cuò)了歡迎和我說……

/**
 * 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這個(gè)event
        // 在RecyclerView的item是clickable的時(shí)候很容易失效,
        // 導(dǎo)致downPositionY不更新,會(huì)有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) {
                    // 這里手動(dòng)調(diào)了下stopScroll,是因?yàn)槊看未髨D收起來之后
                    // item的點(diǎn)擊事件會(huì)有一次失效,推測是這次點(diǎn)擊被用來停止?jié)L動(dòng)了,所以手動(dòng)給他停下
                    stopScroll();
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // 這里即松手判斷大圖位置是不是變了,變了就自動(dòng)收起/折疊
                scrollTop = false;
                if (coordinatorListener.isBeingDragged()) {
                    coordinatorListener.onSwitch();
                    return true;
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

    private boolean isScrollTop(MotionEvent ev) {
        // 在折疊狀態(tài)下,RecyclerView依然是可以上下滾的,
        // 只有RecyclerView下拉到頭馬上要把上面折疊的大圖拽下來了時(shí)是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下拉時(shí)被拉到頭了
                // 一般情況下下面兩個(gè)條件必定有一個(gè)為true,所以這里用&&
                // 這里的邏輯我也修改過,大致意思是第一個(gè)圖片toolbar底部的高度等于decoration或者margin
                // 根據(jù)情況可以自己添加,因?yàn)檫@里出錯(cuò)會(huì)導(dǎo)致折疊的大圖拉不下來
                if ((null != params && gridLayoutManager.findViewByPosition(0).getTop() != params.topMargin) &&
                        gridLayoutManager.findViewByPosition(0).getTop() != gridLayoutManager.getTopDecorationHeight(gridLayoutManager.findViewByPosition(0))) {
                    return false;
                }
                if (!scrollTop) {
                    // 這里的dragDistanceY即大圖折疊時(shí)RecyclerView被拽著滾動(dòng)的距離
                    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調(diào)用得自己加上,作用是滾過一段距離之后自動(dòng)展開
        super.onScrolled(dx, dy);
        totalY += dy;
        if ((totalY > onSwitchDistance || totalY < -onSwitchDistance) && coordinatorListener.isWholeState()) {
            coordinatorListener.switchToTop();
            totalY = 0;
        }
    }

    public void onItemClick(int position) {
        // 自己寫的,搞這個(gè)是為了點(diǎn)擊item時(shí)大圖能展開,且item移動(dòng)到大圖正下面
        if (null == coordinatorListener) {
            return;
        }
        GridLayoutManager manager = (GridLayoutManager) getLayoutManager();
        int firstPosition = manager.findFirstVisibleItemPosition();
        int availablePosition = position - firstPosition;
        // 如果position大于屏幕中顯示的child數(shù)量就會(huì)為空,所以這里要減去
        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; // 大照片高度,最大滑動(dòng)距離
    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) {
        // 初始化這些值,這些定義錯(cuò)了這個(gè)類是沒法實(shí)現(xiàn)效果的
        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;
                // 收起且點(diǎn)在最頂上,在這里處理,這里用getY和getRawY是會(huì)有區(qū)別的,看情況用吧
                if (state == COLLAPSE_STATE && rawY < topBarHeight) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // 應(yīng)該是只有碰到最頂上了才會(huì)走到這里
        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 是按下位置 - 手指拖動(dòng)后的位置
        if (y < topViewHeight && state == WHOLE_STATE && getScrollY() < getScrollRange()) {
            // 展開,手指在滑動(dòng)區(qū)間(toolbar + 正方形)且在范圍內(nèi)(正方形高度)
            beingDragged = true;
            // 手指當(dāng)前位置和開始滑動(dòng)的位置的距離
            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() {
        // 重寫這個(gè)來讓LinearLayout可以滾動(dòng)
        if (scroller.computeScrollOffset()) {
            setScrollY(scroller.getCurrY());
            postInvalidate();
        }
    }

    private int getScrollRange() {
        return maxScrollDistance;
    }

    @Override
    public boolean isWholeState() {
        return state == WHOLE_STATE;
    }
}

使用方法:
分別find出對(duì)象,然后將CoordinatorLinearLayout調(diào)用setCoordinatorListener給CoordinatorRecyclerView就好了
然后調(diào)用CoordinatorLinearLayout的setTopViewParam設(shè)置高度
至于RecyclerView的設(shè)置manager和adapter啥的就不說了

注意事項(xiàng):
設(shè)置高度不要出錯(cuò),一個(gè)toolbar+大照片高度,一個(gè)toolbar高度
記得也給RecyclerView重設(shè)下高度,不然它劃上去也只有被啃剩下那點(diǎn)高度,(一些別的情況下高度設(shè)置可能會(huì)失效,這個(gè)我就不管了……Google吧)
設(shè)置RV的高度時(shí)注意設(shè)置成它最大能展示在屏幕里的高度,設(shè)多了最后滾到最下面會(huì)顯示不全

可能的問題

  1. 折疊上去以后第一次點(diǎn)擊失效:可能是RecyclerView的ScrollState沒更新導(dǎo)致的
  2. 折疊上去之后拽不下來:downPositionY位置沒更新導(dǎo)致的
  3. 別的我沒發(fā)現(xiàn)的問題
    前兩個(gè)在注釋里有提到原因和解決辦法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容