Androidは京東、天猫appの商品詳細ページのレイアウトアーキテクチャ、機能実現を模倣

26176 ワード

商品詳細ページ
電子商取引の種類のappの子供靴をする必要があることがあって見て、まず効果を見て実現します
  • 開発ツール:
  • Android Studio
  • で使用するサードパーティ製フレーム:
  • ネットワークピクチャをロードするために使用されるFresco
  • 頭部の商品図輪播ConvenientBanner
  • ナビゲーションバー切り替えPagerSlidingTabStrip
  • プロジェクトソース
  • 効果図
    コード量が多すぎるため、いくつかのコアのカスタムコントロールだけを紹介したくない子供靴はapkをダウンロードしたりgithubでソースコードをダウンロードしたりして使用することができます.
  • 最外層のレイアウトファイル
  • 
    
         
        
    
            
    
                
    
                    
                
    
                
    
                    
                    
    
                    
                
            
        
    
         
        
    
    
    
  • ItemWebViewはSlideDetailsLayoutのサブView(SlideDetailsLayoutコードが多すぎて最後に置いた)
  • です
  • 機能は、商品概要を表示するWebview
  • である.
  • は、上方へスライドするときに第1のView
  • に直接スライドすることを防止する.
  • WebViewの上部にスライドする場合、親コントロールにタッチイベント
  • を再取得させる.
    /**
     *         webview
     */
    public class ItemWebView extends WebView {
        public float oldY;
        private int t;
        private float oldX;
    
        public ItemWebView(Context context) {
            super(context);
        }
    
        public ItemWebView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public ItemWebView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    float Y = ev.getY();
                    float Ys = Y - oldY;
                    float X = ev.getX();
    
                    //                 
                    if (Ys > 0 && t == 0) {
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
    
                case MotionEvent.ACTION_DOWN:
                    getParent().getParent().requestDisallowInterceptTouchEvent(true);
                    oldY = ev.getY();
                    oldX = ev.getX();
                    break;
    
                case MotionEvent.ACTION_UP:
                    getParent().getParent().requestDisallowInterceptTouchEvent(true);
                    break;
    
                default:
                    break;
            }
            return super.onTouchEvent(ev);
        }
    
        @Override
        protected void onScrollChanged(int l, int t, int oldl, int oldt) {
            this.t = t;
            super.onScrollChanged(l, t, oldl, oldt);
        }
    
    }
    
  • ItemListViewもSlideDetailsLayoutのサブView
  • である.
  • ItemWebViewの機能とほぼ同じ
  • です.
  • もちろんここではListViewだけでなく、他のコントロールもSlideDetailsLayoutのスライド機能
  • に適合するように書くことができる.
    /**
     *         ListView
     */
    public class ItemListView extends ListView implements AbsListView.OnScrollListener {
        private float oldX, oldY;
        private int currentPosition;
    
        public ItemListView(Context context) {
            super(context);
            setOnScrollListener(this);
        }
    
        public ItemListView(Context context, AttributeSet attrs) {
            super(context, attrs);
            setOnScrollListener(this);
        }
    
        public ItemListView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            setOnScrollListener(this);
        }
    
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    float Y = ev.getY();
                    float Ys = Y - oldY;
                    float X = ev.getX();
                    int [] location = new int [2];
                    getLocationInWindow(location);
    
                    //                 
                    if (Ys > 0 && currentPosition == 0) {
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
    
                case MotionEvent.ACTION_DOWN:
                    getParent().getParent().requestDisallowInterceptTouchEvent(true);
                    oldY = ev.getY();
                    oldX = ev.getX();
                    break;
    
                case MotionEvent.ACTION_UP:
                    getParent().getParent().requestDisallowInterceptTouchEvent(true);
                    break;
    
                default:
                    break;
            }
            return super.onTouchEvent(ev);
        }
    
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            currentPosition = getFirstVisiblePosition();
        }
    
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    
        }
    }
    
  • NoScrollViewPager最外層の親レイアウト
  • 図文詳細モジュールにスライドすると、ViewPagerのスライドイベント
  • を禁止することができる.
  • 大体意味はスライドして図文の詳細についた時に直接右にスライドできない方式で詳細と評価モジュールに切り替える(京東淘宝機能参照)
  • .
    /**
     *             ViewPager
     */
    public class NoScrollViewPager extends ViewPager {
        private boolean noScroll = false;
    
        public NoScrollViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
    
        public NoScrollViewPager(Context context) {
            super(context);
        }
    
        public void setNoScroll(boolean noScroll) {
            this.noScroll = noScroll;
        }
    
        @Override
        public void scrollTo(int x, int y) {
            super.scrollTo(x, y);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent arg0) {
            if (noScroll)
                return false;
            else
                return super.onTouchEvent(arg0);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent arg0) {
            if (noScroll)
                return false;
            else
                return super.onInterceptTouchEvent(arg0);
        }
    
        @Override
        public void setCurrentItem(int item, boolean smoothScroll) {
            super.setCurrentItem(item, smoothScroll);
        }
    
        @Override
        public void setCurrentItem(int item) {
            super.setCurrentItem(item);
        }
    
    }
    
  • 商品モジュールの最外層のレイアウトは、SlideDetailsLayoutというカスタムView Groupの
  • です.SlideDetailsLayoutの内容は2つのViewmFrontView(第1のView)とmBehindView(第2のView)の2つの状態があり、状態がcloseに設定されると第1の商品データViewが表示され、open状態は第2の図文詳細Viewが表示される
    @SuppressWarnings("unused")
    public class SlideDetailsLayout extends ViewGroup {
    
        /**
         * Callback for panel OPEN-CLOSE status changed.
         */
        public interface OnSlideDetailsListener {
            /**
             * Called after status changed.
             *
             * @param status {@link Status}
             */
            void onStatucChanged(Status status);
        }
    
        public enum Status {
            /** Panel is closed */
            CLOSE,
            /** Panel is opened */
            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();
        }
    
        /**
         * Set the callback of panel OPEN-CLOSE status.
         *
         * @param listener {@link OnSlideDetailsListener}
         */
        public void setOnSlideDetailsListener(OnSlideDetailsListener listener) {
            this.mOnSlideDetailsListener = listener;
        }
    
        /**
         * Open pannel smoothly.
         *
         * @param smooth true, smoothly. false otherwise.
         */
        public void smoothOpen(boolean smooth) {
            if (mStatus != Status.OPEN) {
                mStatus = Status.OPEN;
                final float height = -getMeasuredHeight();
                animatorSwitch(0, height, true, smooth ? mDuration : 0);
            }
        }
    
        /**
         * Close pannel smoothly.
         *
         * @param smooth true, smoothly. false otherwise.
         */
        public void smoothClose(boolean smooth) {
            if (mStatus != Status.CLOSE) {
                mStatus = Status.CLOSE;
                final float height = -getMeasuredHeight();
                animatorSwitch(height, 0, true, smooth ? mDuration : 0);
            }
        }
    
        /**
         * Set the float value for indicate the moment of switch panel
         *
         * @param percent (0.0, 1.0)
         */
        public void setPercent(float percent) {
            this.mPercent = percent;
        }
    
        @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() {
            super.onFinishInflate();
    
            final int childCount = getChildCount();
            if (1 >= childCount) {
                throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!");
            }
    
            mFrontView = getChildAt(0);
            mBehindView = getChildAt(1);
    
            // set behindview's visibility to GONE before show.
            //mBehindView.setVisibility(GONE);
            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);
                // skip measure if gone
                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);
    
                // skip layout
                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);
    
                        // intercept rules:
                        // 1. The vertical displacement is larger than the horizontal displacement;
                        // 2. Panel stauts is CLOSE:slide up
                        // 3. Panel status is OPEN:slide down
                        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 target is a view, we want the DOWN action.
                    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;
        }
    
        /**
         * @param offset Displacement in vertically.
         */
        private void processTouchEvent(final float offset) {
            if (Math.abs(offset) < mTouchSlop) {
                return;
            }
    
            final float oldOffset = mSlideOffset;
            // pull up to open
            if (mStatus == Status.CLOSE) {
                // reset if pull down
                if (offset >= 0) {
                    mSlideOffset = 0;
                } else {
                    mSlideOffset = offset;
                }
    
                if (mSlideOffset == oldOffset) {
                    return;
                }
    
                // pull down to close
            } else if (mStatus == Status.OPEN) {
                final float pHeight = -getMeasuredHeight();
                // reset if pull up
                if (offset <= 0) {
                    mSlideOffset = pHeight;
                } else {
                    final float newOffset = pHeight + offset;
                    mSlideOffset = newOffset;
                }
    
                if (mSlideOffset == oldOffset) {
                    return;
                }
            }
            // relayout
            requestLayout();
        }
    
        /**
         * Called after gesture is ending.
         */
        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 {
                    // keep panel closed
                    mSlideOffset = 0;
                }
            } else if (Status.OPEN == mStatus) {
                if ((offset + pHeight) >= percent) {
                    mSlideOffset = 0;
                    mStatus = Status.CLOSE;
                    changed = true;
                } else {
                    // keep panel opened
                    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.onStatucChanged(mStatus);
                        }
                    }
                }
            });
            animator.setDuration(duration);
            animator.start();
        }
    
        /**
         * Whether the closed pannel is opened at first time.
         * If open first, we should set the behind view's visibility as VISIBLE.
         */
        private void checkAndFirstOpenPanel() {
            if (isFirstShowBehindView) {
                isFirstShowBehindView = false;
                mBehindView.setVisibility(VISIBLE);
            }
        }
    
        /**
         * When pulling, target view changed by the panel status. If panel opened, the target is behind view.
         * Front view is for otherwise.
         */
        private void ensureTarget() {
            if (mStatus == Status.CLOSE) {
                mTarget = mFrontView;
            } else {
                mTarget = mBehindView;
            }
        }
    
        /**
         * Check child view can srcollable in vertical direction.
         *
         * @param direction Negative to check scrolling up, positive to check scrolling down.
         *
         * @return true if this view can be scrolled in the specified direction, false otherwise.
         */
        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;
    
            /**
             * Constructor used when reading from a parcel. Reads the state of the superclass.
             *
             * @param source
             */
            public SavedState(Parcel source) {
                super(source);
                offset = source.readFloat();
                status = source.readInt();
            }
    
            /**
             * Constructor called by derived classes when creating their SavedState objects
             *
             * @param superState The state of the superclass of this view
             */
            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 CREATOR =
                    new Creator() {
                        public SavedState createFromParcel(Parcel in) {
                            return new SavedState(in);
                        }
    
                        public SavedState[] newArray(int size) {
                            return new SavedState[size];
                        }
                    };
        }
    }
    

    この商品の詳細ページのアーキテクチャも本人がオンラインになったプロジェクトで使用しています
    star、forkへようこそ、ありがとう
  • githubアドレス