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的高度时注意设置成它最大能展示在屏幕里的高度,设多了最后滚到最下面会显示不全
可能的问题
- 折叠上去以后第一次点击失效:可能是RecyclerView的ScrollState没更新导致的
- 折叠上去之后拽不下来:downPositionY位置没更新导致的
- 别的我没发现的问题
前两个在注释里有提到原因和解决办法