仿京东、天猫app的商品详情页的布局架构, 以及功能实现

JackieMolin 7年前
   <h2><strong>GoodsInfoPage</strong></h2>    <p>有需要做电商类app的童鞋可以看看, 首先先看看效果实现</p>    <p>本项目使用的第三方框架:</p>    <ol>     <li> <p>加载网络图片使用的 <a href="/misc/goto?guid=4958869680969840391" rel="nofollow,noindex">Fresco</a></p> </li>     <li> <p>头部的商品图轮播 <a href="/misc/goto?guid=4959722893575543444" rel="nofollow,noindex">ConvenientBanner</a></p> </li>     <li> <p>导航栏切换 <a href="/misc/goto?guid=4958988893073775819" rel="nofollow,noindex">PagerSlidingTabStrip</a></p> </li>    </ol>    <h3><strong>效果图如下</strong></h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d41d84b596595245ec4404ec3b15df36.gif"></p>    <p style="text-align:center">效果实现</p>    <p>由于代码量过多, 就不一一讲解只介绍几个核心的自定义控件)</p>    <ul>     <li> <h3><strong>最外层的布局文件</strong></h3> </li>    </ul>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:app="http://schemas.android.com/apk/res-auto"      xmlns:tools="http://schemas.android.com/tools"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:orientation="vertical">       <!-- 顶部标题 -->      <LinearLayout          android:id="@+id/ll_title_root"          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:background="#ec0f38"          android:orientation="vertical">            <LinearLayout              android:layout_width="match_parent"              android:layout_height="44dp"              android:orientation="horizontal">                <LinearLayout                  android:id="@+id/ll_back"                  android:layout_width="wrap_content"                  android:layout_height="match_parent"                  android:paddingLeft="15dp">                    <ImageView                      android:id="@+id/iv_back"                      android:layout_width="22dp"                      android:layout_height="22dp"                      android:layout_gravity="center_vertical"                      android:src="@mipmap/address_come_back" />              </LinearLayout>                <LinearLayout                  android:layout_width="0dp"                  android:layout_height="match_parent"                  android:layout_weight="1"                  android:gravity="center">                    <!-- 商品、详情、评价切换的控件 -->                  <com.gxz.PagerSlidingTabStrip                      android:id="@+id/psts_tabs"                      android:layout_width="wrap_content"                      android:layout_height="32dp"                      android:layout_gravity="center"                      android:textColor="#ffffff"                      android:textSize="15sp"                      app:pstsDividerColor="@android:color/transparent"                      app:pstsDividerPaddingTopBottom="0dp"                      app:pstsIndicatorColor="#ffffff"                      app:pstsIndicatorHeight="2dp"                      app:pstsScaleZoomMax="0.0"                      app:pstsShouldExpand="false"                      app:pstsSmoothScrollWhenClickTab="false"                      app:pstsTabPaddingLeftRight="12dp"                      app:pstsTextAllCaps="false"                      app:pstsTextSelectedColor="#ffffff"                      app:pstsUnderlineHeight="0dp" />                    <TextView                      android:id="@+id/tv_title"                      android:layout_width="wrap_content"                      android:layout_height="wrap_content"                      android:text="图文详情"                      android:textColor="#ffffff"                      android:textSize="15sp"                      android:visibility="gone" />              </LinearLayout>          </LinearLayout>      </LinearLayout>         <!-- 功能下面有介绍 -->      <com.hq.hsmwan.widget.NoScrollViewPager          android:id="@+id/vp_content"          android:layout_width="match_parent"          android:layout_height="0dp"          android:layout_weight="1" />  </LinearLayout></code></pre>    <ul>     <li> <h3><strong>ItemWebView是 SlideDetailsLayout 的子View (SlideDetailsLayout代码太多, 放到了最后)</strong></h3>      <ul>       <li> <p>功能为显示商品简介的webview</p> </li>       <li> <p>防止往上滑动时会直接滑动到第一个View</p> </li>       <li> <p>实现滑动到WebView顶部时, 让父控件重新获得触摸事件</p> </li>      </ul> </li>    </ul>    <pre>  <code class="language-java">/**   * 商品详情页底部的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);      }    }</code></pre>    <ul>     <li> <h3><strong>ItemListView 也是 SlideDetailsLayout 的子View</strong></h3>      <ul>       <li> <p>和ItemWebView功能大致一样</p> </li>      </ul> </li>    </ul>    <pre>  <code class="language-java">/**   * 商品详情页底部的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) {        }  }</code></pre>    <ul>     <li> <h3><strong>NoScrollViewPager为最外层的父布局</strong></h3>      <ul>       <li> <p>当滑动到图文详情模块时, 能禁止掉ViewPager的滑动事件</p> </li>      </ul> </li>    </ul>    <pre>  <code class="language-java">/**   * 提供禁止滑动功能的自定义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);      }    }</code></pre>    <ul>     <li>商品模块最外层的布局是一个自定义的ViewGroup名为 SlideDetailsLayout <p>SlideDetailsLayout 内容有两个View, mFrontView (第一个View)和 mBehindView (第二个View)</p> <p>有两种状态, 状态设置为close就显示第一个商品数据View, open状态就显示第二个图文详情View</p> </li>    </ul>    <pre>  <code class="language-java">@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<SavedState> CREATOR =                  new Creator<SavedState>() {                      public SavedState createFromParcel(Parcel in) {                          return new SavedState(in);                      }                        public SavedState[] newArray(int size) {                          return new SavedState[size];                      }                  };      }  }</code></pre>    <p>这个商品详情页的架构也是本人在已上线的项目中使用</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/0b5341742ab5</p>    <p> </p>