Android仿京东、天猫商品详情页详解手机开发

前言

前面在介绍控件TabLayout控件和CoordinatorLayout使用的时候说了下实现京东、天猫详情页面的效果,今天要说的是优化版,是我们线上实现的效果,首先看一下效果:
这里写图片描述
这里写图片描述

项目结构分析

首先我们来分析一下要实现上面的效果,我们需要怎么做。顶部是一个可以滑动切换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() { 
                @Override 
                public 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); 
        } 
 
        @Override 
        public 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等的页面填充,也没啥好讲的,代码又很多可以优化的地方,在优化的地方,笔者也列出了优化的方案,大家可以根据自己的实际情况做页面级的优化。
附:Android仿京东、天猫商品详情页源码

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5874.html

(0)
上一篇 2021年7月17日 00:21
下一篇 2021年7月17日 00:21

相关推荐

发表回复

登录后才能评论