项目结构分析
首先我们来分析一下要实现上面的效果,我们需要怎么做。顶部是一个可以滑动切换Tab,可以用ViewPager+Fragment实现,也可以使用系统的TabLayout控件实现;而下面的 View是一个可以滑动拖动效果的View,可以采用网上一个叫做DragLayout的控件,我这里是自己实现了一个,主要是通过对View的事件分发的一些处理;然后滑动到下面就是一个图文详情的View(Fragment),本页面包含两个界面:详情页面和参数页面;最后是评价的View(Fragment)。经过上面的分析,我们的界面至少需要4个Fragement,首先来看一下项目结构:
代码讲解
代码比较多,这里只讲解几个核心的方法类。首先我们来看一下我们自己是的这个具有阻尼效果的View,我们知道要实现的效果,我们需要对View的事件做一个全面的实现。这里首先说一下View的事件分发的流程:
onInterceptTouchEvent()–>dispatchTouchEvent()–>onTouchEvent();
首先我们需要对View传过来的事件做一个拦截:
ensureTarget();if (null == mTarget) { return false;}if (!isEnabled()) { return false;}final int aciton = MotionEventCompat.getActionMasked(ev);boolean shouldIntercept = false;switch (aciton) { case MotionEvent.ACTION_DOWN: {mInitMotionX = ev.getX();mInitMotionY = ev.getY();shouldIntercept = false;break; } case MotionEvent.ACTION_MOVE: {final float x = ev.getX();final float y = ev.getY();final float xDiff = x - mInitMotionX;final float yDiff = y - mInitMotionY;if (canChildScrollVertically((int) yDiff)) { shouldIntercept = false;} else { final float xDiffabs = Math.abs(xDiff); final float yDiffabs = Math.abs(yDiff); if (yDiffabs > mTouchSlop && yDiffabs >= xDiffabs && !(mStatus == Status.CLOSE && yDiff > 0 || mStatus == Status.OPEN && yDiff < 0)) {shouldIntercept = true; }}break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: {shouldIntercept = false;break; }}return shouldIntercept;最后转发给onTouchEvent
ensureTarget();if (null == mTarget) { return false;}if (!isEnabled()) { return false;}boolean wantTouch = true;final int action = MotionEventCompat.getActionMasked(ev);switch (action) { case MotionEvent.ACTION_DOWN: {if (mTarget instanceof View) { wantTouch = true;}break; } case MotionEvent.ACTION_MOVE: {final float y = ev.getY();final float yDiff = y - mInitMotionY;if (canChildScrollVertically(((int) yDiff))) { wantTouch = false;} else { processTouchEvent(yDiff); wantTouch = true;}break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: {finishTouchEvent();wantTouch = false;break; }}return wantTouch;滑动事件完了之后我们需要调用request方法对View做一个重绘:
final int left = l;final int right = r;int top;int bottom;final int offset = (int) mSlideOffset;View child;for (int i = 0; i < getChildCount(); i++) { child = getChildAt(i); if (child.getVisibility() == GONE) {continue; } if (child == mBehindView) {top = b + offset;bottom = top + b - t; } else {top = t + offset;bottom = b + offset; } child.layout(left, top, right, bottom);}上下滑动也是涉及到两个界面:mFrontView和mBehindView,然后通过判断滑动事件来显示哪一个View。具体看代码:
package com.xzh.gooddetail.view;import android.animation.Animator;import android.animation.AnimatorListenerAdapter;import android.animation.ValueAnimator;import android.content.Context;import android.content.res.TypedArray;import android.os.Parcel;import android.os.Parcelable;import android.support.v4.view.MotionEventCompat;import android.support.v4.view.ViewCompat;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.ViewConfiguration;import android.view.ViewGroup;import android.widget.AbsListView;import android.widget.FrameLayout;import android.widget.LinearLayout;import android.widget.RelativeLayout;import com.xzh.gooddetail.R;public class SlideDetailsLayout extends ViewGroup { public interface OnSlideDetailsListener {void onStatusChanged(Status status); } public enum Status {CLOSE,OPEN;public static Status valueOf(int stats) { if (0 == stats) {return CLOSE; } else if (1 == stats) {return OPEN; } else {return CLOSE; }} } private static final float DEFAULT_PERCENT = 0.2f; private static final int DEFAULT_DURATION = 300; private View mFrontView; private View mBehindView; private float mTouchSlop; private float mInitMotionY; private float mInitMotionX; private View mTarget; private float mSlideOffset; private Status mStatus = Status.CLOSE; private boolean isFirstShowBehindView = true; private float mPercent = DEFAULT_PERCENT; private long mDuration = DEFAULT_DURATION; private int mDefaultPanel = 0; private OnSlideDetailsListener mOnSlideDetailsListener; public SlideDetailsLayout(Context context) {this(context, null); } public SlideDetailsLayout(Context context, AttributeSet attrs) {this(context, attrs, 0); } public SlideDetailsLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideDetailsLayout, defStyleAttr, 0);mPercent = a.getFloat(R.styleable.SlideDetailsLayout_percent, DEFAULT_PERCENT);mDuration = a.getInt(R.styleable.SlideDetailsLayout_duration, DEFAULT_DURATION);mDefaultPanel = a.getInt(R.styleable.SlideDetailsLayout_default_panel, 0);a.recycle();mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } public void setOnSlideDetailsListener(OnSlideDetailsListener listener) {this.mOnSlideDetailsListener = listener; } public void smoothOpen(boolean smooth) {if (mStatus != Status.OPEN) { mStatus = Status.OPEN; final float height = -getMeasuredHeight(); animatorSwitch(0, height, true, smooth ? mDuration : 0);} } public void smoothClose(boolean smooth) {if (mStatus != Status.CLOSE) { mStatus = Status.CLOSE; final float height = -getMeasuredHeight(); animatorSwitch(height, 0, true, smooth ? mDuration : 0);} } @Override protected LayoutParams generateDefaultLayoutParams() {return new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) {return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) {return new MarginLayoutParams(p); } @Override protected void onFinishInflate() {final int childCount = getChildCount();if (1 >= childCount) { throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!");}mFrontView = getChildAt(0);mBehindView = getChildAt(1);if (mDefaultPanel == 1) { post(new Runnable() {@Overridepublic void run() { smoothOpen(false);} });} } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {final int pWidth = MeasureSpec.getSize(widthMeasureSpec);final int pHeight = MeasureSpec.getSize(heightMeasureSpec);final int childWidthMeasureSpec =MeasureSpec.makeMeasureSpec(pWidth, MeasureSpec.EXACTLY);final int childHeightMeasureSpec =MeasureSpec.makeMeasureSpec(pHeight, MeasureSpec.EXACTLY);View child;for (int i = 0; i < getChildCount(); i++) { child = getChildAt(i); if (child.getVisibility() == GONE) {continue; } measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);}setMeasuredDimension(pWidth, pHeight); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {final int left = l;final int right = r;int top;int bottom;final int offset = (int) mSlideOffset;View child;for (int i = 0; i < getChildCount(); i++) { child = getChildAt(i); if (child.getVisibility() == GONE) {continue; } if (child == mBehindView) {top = b + offset;bottom = top + b - t; } else {top = t + offset;bottom = b + offset; } child.layout(left, top, right, bottom);} } @Override public boolean onInterceptTouchEvent(MotionEvent ev) {ensureTarget();if (null == mTarget) { return false;}if (!isEnabled()) { return false;}final int aciton = MotionEventCompat.getActionMasked(ev);boolean shouldIntercept = false;switch (aciton) { case MotionEvent.ACTION_DOWN: {mInitMotionX = ev.getX();mInitMotionY = ev.getY();shouldIntercept = false;break; } case MotionEvent.ACTION_MOVE: {final float x = ev.getX();final float y = ev.getY();final float xDiff = x - mInitMotionX;final float yDiff = y - mInitMotionY;if (canChildScrollVertically((int) yDiff)) { shouldIntercept = false;} else { final float xDiffabs = Math.abs(xDiff); final float yDiffabs = Math.abs(yDiff); if (yDiffabs > mTouchSlop && yDiffabs >= xDiffabs && !(mStatus == Status.CLOSE && yDiff > 0 || mStatus == Status.OPEN && yDiff < 0)) {shouldIntercept = true; }}break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: {shouldIntercept = false;break; }}return shouldIntercept; } @Override public boolean onTouchEvent(MotionEvent ev) {ensureTarget();if (null == mTarget) { return false;}if (!isEnabled()) { return false;}boolean wantTouch = true;final int action = MotionEventCompat.getActionMasked(ev);switch (action) { case MotionEvent.ACTION_DOWN: {if (mTarget instanceof View) { wantTouch = true;}break; } case MotionEvent.ACTION_MOVE: {final float y = ev.getY();final float yDiff = y - mInitMotionY;if (canChildScrollVertically(((int) yDiff))) { wantTouch = false;} else { processTouchEvent(yDiff); wantTouch = true;}break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: {finishTouchEvent();wantTouch = false;break; }}return wantTouch; } private void processTouchEvent(final float offset) {if (Math.abs(offset) < mTouchSlop) { return;}final float oldOffset = mSlideOffset;if (mStatus == Status.CLOSE) { // reset if pull down if (offset >= 0) {mSlideOffset = 0; } else {mSlideOffset = offset; } if (mSlideOffset == oldOffset) {return; }} else if (mStatus == Status.OPEN) { final float pHeight = -getMeasuredHeight(); if (offset <= 0) {mSlideOffset = pHeight; } else {final float newOffset = pHeight + offset;mSlideOffset = newOffset; } if (mSlideOffset == oldOffset) {return; }}requestLayout(); } private void finishTouchEvent() {final int pHeight = getMeasuredHeight();final int percent = (int) (pHeight * mPercent);final float offset = mSlideOffset;boolean changed = false;if (Status.CLOSE == mStatus) { if (offset <= -percent) {mSlideOffset = -pHeight;mStatus = Status.OPEN;changed = true; } else {mSlideOffset = 0; }} else if (Status.OPEN == mStatus) { if ((offset + pHeight) >= percent) {mSlideOffset = 0;mStatus = Status.CLOSE;changed = true; } else {mSlideOffset = -pHeight; }}animatorSwitch(offset, mSlideOffset, changed); } private void animatorSwitch(final float start, final float end) {animatorSwitch(start, end, true, mDuration); } private void animatorSwitch(final float start, final float end, final long duration) {animatorSwitch(start, end, true, duration); } private void animatorSwitch(final float start, final float end, final boolean changed) {animatorSwitch(start, end, changed, mDuration); } private void animatorSwitch(final float start,final float end,final boolean changed,final long duration) {ValueAnimator animator = ValueAnimator.ofFloat(start, end);animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) {mSlideOffset = (float) animation.getAnimatedValue();requestLayout(); }});animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) {super.onAnimationEnd(animation);if (changed) { if (mStatus == Status.OPEN) {checkAndFirstOpenPanel(); } if (null != mOnSlideDetailsListener) {mOnSlideDetailsListener.onStatusChanged(mStatus); }} }});animator.setDuration(duration);animator.start(); } private void checkAndFirstOpenPanel() {if (isFirstShowBehindView) { isFirstShowBehindView = false; mBehindView.setVisibility(VISIBLE);} } private void ensureTarget() {if (mStatus == Status.CLOSE) { mTarget = mFrontView;} else { mTarget = mBehindView;} } protected boolean canChildScrollVertically(int direction) {if (mTarget instanceof AbsListView) { return canListViewSroll((AbsListView) mTarget);} else if (mTarget instanceof FrameLayout ||mTarget instanceof RelativeLayout ||mTarget instanceof LinearLayout) { View child; for (int i = 0; i < ((ViewGroup) mTarget).getChildCount(); i++) {child = ((ViewGroup) mTarget).getChildAt(i);if (child instanceof AbsListView) { return canListViewSroll((AbsListView) child);} }}if (android.os.Build.VERSION.SDK_INT < 14) { return ViewCompat.canScrollVertically(mTarget, -direction) || mTarget.getScrollY() > 0;} else { return ViewCompat.canScrollVertically(mTarget, -direction);} } protected boolean canListViewSroll(AbsListView absListView) {if (mStatus == Status.OPEN) { return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) .getTop() < absListView.getPaddingTop());} else { final int count = absListView.getChildCount(); return count > 0 && (absListView.getLastVisiblePosition() < count - 1 || absListView.getChildAt(count - 1) .getBottom() > absListView.getMeasuredHeight());} } @Override protected Parcelable onSaveInstanceState() {SavedState ss = new SavedState(super.onSaveInstanceState());ss.offset = mSlideOffset;ss.status = mStatus.ordinal();return ss; } @Override protected void onRestoreInstanceState(Parcelable state) {SavedState ss = (SavedState) state;super.onRestoreInstanceState(ss.getSuperState());mSlideOffset = ss.offset;mStatus = Status.valueOf(ss.status);if (mStatus == Status.OPEN) { mBehindView.setVisibility(VISIBLE);}requestLayout(); } static class SavedState extends BaseSavedState {private float offset;private int status;public SavedState(Parcel source) { super(source); offset = source.readFloat(); status = source.readInt();}public SavedState(Parcelable superState) { super(superState);}@Overridepublic void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeFloat(offset); out.writeInt(status);}public static final Creator<SavedState> CREATOR =new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) {return new SavedState(in); } public SavedState[] newArray(int size) {return new SavedState[size]; }}; }}接下来就是一些Fragment等的页面填充,也没啥好讲的,代码又很多可以优化的地方,在优化的地方,笔者也列出了优化的方案,大家可以根据自己的实际情况做页面级的优化。