这是一个复杂的控件,双层布局,可滑动,可拖拽
Demo下载:我的Github DoublePull
格瓦拉实际效果图:
简单分析:
- 根部局使用了RelativeLayout,有两个子布局:外层布局与内层布局。
- 外层布局。根布局为自定义ScrollView,有两子布局:HeaderFrameLayout与PullRelativeLayout
- 内层布局。根部局为RelativeLayout,有两子布局:RecyclerView与ImageButton
外层布局如图:
内层布局如图:
实现难点:
- 初始化,事件由ScrollView处理和消耗。当滚动到顶部,若继续往下滑动,事件由子View PullRelativeLayout处理和消耗
- 若PullRelativeLayout处理和消耗事件,拖拽距离过小,移动到原来位置。反之,则向下滑动隐藏布局
- HeaderFrameLayout随着PullRelativeLayout的变化而变化。隐藏则一起隐藏,打开则一起打开
- PullRelativeLayout隐藏后,ScrollView将不能处理和消耗事件,事件应由RecyclerView处理和消耗
- RecyclerView头布局滚动高度超过,头的70%,PullRelativeLayout做动画。HeaderFrameLayout做打开动画
- HeaderFrameLayout自定义View内部需要有打开功能,可使用Scroller来完成
- PullRelativeLayout自定义View内部需要有打开和隐藏功能,可使用Scroller来完成
- View的滑动也可使用动画的形式,但是由于需要设置一些Visibility属性,这里就使用Scroller来完成滑动
- 自定义View之间的状态获取和数据交互,使用了对外提供回调接口的形式和对象直接注入的形式
- 调试布局效果会花掉一些时间
自定义View分析:
OutScrollView
public class OutScrollView extends ScrollView {
private int mPullRelativeLayoutState = PullRelativeLayout.SHOW;
private int mDownY;
private int mMoveY;
private OnScrollStateChangeListener mOnScrollStateChangeListener;
public OutScrollView(Context context) {
super(context);
}
public OutScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public OutScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setPullRelativeLayoutState(int state) {
mPullRelativeLayoutState = state;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mOnScrollStateChangeListener != null) {
mOnScrollStateChangeListener.onScrollChange(l, t, oldl, oldt);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mMoveY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mDownY = 0;
mMoveY = 0;
break;
}
if(mMoveY - mDownY < 0) {
return super.onInterceptTouchEvent(ev);
}
if (getScrollY() == 0) {
if (mPullRelativeLayoutState == PullRelativeLayout.SHOW) {
return false;
}
if (mPullRelativeLayoutState == PullRelativeLayout.MOVE) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mPullRelativeLayoutState == PullRelativeLayout.HIDE) {
return false;
}
return super.onTouchEvent(ev);
}
public interface OnScrollStateChangeListener {
void onScrollChange(int l, int t, int oldl, int oldt);
}
public void setOnScrollStateChangeListener(OnScrollStateChangeListener listener) {
mOnScrollStateChangeListener = listener;
}
}
- 获取PullRelativeLayoutState的状态值,根据它来判断是否处理事件:自己处理,子View处理,不处理等
- OnScrollStateChangeListener把ScrollView滚动高度状态回调出去,给外面使用
HeaderFrameLayout
public class HeaderFrameLayout extends FrameLayout {
private Scroller mScroller;
private int mHeight;
private boolean isOpen;
public HeaderFrameLayout(Context context) {
super(context);
init();
}
public HeaderFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HeaderFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mScroller = new Scroller(getContext());
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHeight = getMeasuredHeight();
}
public void setOpen(boolean flag) {
isOpen = flag;
}
public boolean isOpen() {
return isOpen;
}
public void open() {
if (!isOpen) {
return;
}
smoothScrollTo(0, mHeight, 0, -mHeight, 800);
isOpen = false;
}
private void smoothScrollTo(int startX, int startY,
int dx, int dy, int duration) {
mScroller.startScroll(startX, startY, dx, dy, duration);
invalidate();
}
}
- 使用Scroller来滑动,让自己拥有打开滑动的效果,初始化位移高度等
- 一个boolean值来限制是否做打开滑动的效果
PullRelativeLayout
public class PullRelativeLayout extends RelativeLayout {
public static final int SHOW = 1000;
public static final int HIDE = 2000;
public static final int MOVE = 3000;
public static final int OPEN_START = 4000;
public static final int OPEN_FINISH = 5000;
private static final int NORMAL_TIME = 600;
private int mMaxOffset;
private int mState = SHOW;
private float mLastY;
private int mMoveY;
private Scroller mScroller;
private OnStateChangeListener mOnStateChangeListener;
public PullRelativeLayout(Context context) {
super(context);
init();
}
public PullRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PullRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mScroller = new Scroller(getContext());
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void setMaxOffset(int offset) {
mMaxOffset = offset;
}
public void setState(int state) {
mState = state;
}
public int getState() {
return mState;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mState == HIDE) {
return false;
}
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int moveY = (int) (y - mLastY);
if (getScrollY() <= 0 && moveY > 0) {
int offset = moveY / 2;
move(offset);
}
mLastY = y;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
changeState();
break;
}
return true;
}
private void move(int offset) {
mState = MOVE;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.pullViewMove(mState, -offset);
}
scrollBy(0, -offset);
}
private void hide() {
mState = HIDE;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.pullViewHide(mState);
}
mMoveY = getMeasuredHeight() + Math.abs(getScrollY());
smoothScrollTo(0, getScrollY(), 0, -mMoveY, NORMAL_TIME * 3);
}
public void hide(int time) {
mState = HIDE;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.pullViewHide(mState);
}
mMoveY = getMeasuredHeight() + Math.abs(getScrollY());
smoothScrollTo(0, getScrollY(), 0, -mMoveY, time);
}
private void show() {
mState = SHOW;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.pullViewShow(mState);
}
smoothScrollTo(0, getScrollY(), 0, -getScrollY(), getScrollY());
}
private void changeState() {
if (Math.abs(getScrollY()) > mMaxOffset + 50) {
hide();
} else {
show();
}
}
public void open() {
mState = OPEN_START;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.pullViewOpenStart();
}
smoothScrollTo(0, -mMoveY, 0, mMoveY, NORMAL_TIME);
postDelayed(new Runnable() {
@Override
public void run() {
mState = OPEN_FINISH;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.pullViewOpenFinish();
}
}
}, NORMAL_TIME);
}
private void smoothScrollTo(int startX, int startY,
int dx, int dy, int duration) {
mScroller.startScroll(startX, startY, dx, dy, duration);
invalidate();
}
public interface OnStateChangeListener {
void pullViewShow(int state);
void pullViewHide(int state);
void pullViewMove(int state, int offset);
void pullViewOpenStart();
void pullViewOpenFinish();
}
public void setOnStateChangeListener(OnStateChangeListener listener) {
mOnStateChangeListener = listener;
}
}
- 使用Scroller来滑动,设置打开,隐藏,手指拖拽的方法。将View的各种状态回调出去,用于同步其他View的状态
- 设置状态监听器
再来看一次效果图:
Demo下载:我的Github DoublePull
2016年7月09日 03:57:12