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

2017-01-11 16:02:20来源:http://www.jianshu.com/p/0b5341742ab5作者:coexist人点击

GoodsInfoPage
有需要做电商类app的童鞋可以看看, 首先先看看效果实现
本项目使用的第三方框架:
加载网络图片使用的 Fresco
头部的商品图轮播 ConvenientBanner
导航栏切换 PagerSlidingTabStrip


先看看效果实现

效果实现
由于代码量过多, 就不一一讲解只介绍几个核心的自定义控件)
不想看的童鞋可以下载apk或者在github上下载源码使用
github地址

apk下载



最外层的布局文件

<?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>


ItemWebView是SlideDetailsLayout的子View (SlideDetailsLayout代码太多, 放到了最后)
功能为显示商品简介的webview
防止往上滑动时会直接滑动到第一个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
*/
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);
}
}


商品模块最外层的布局是一个自定义的ViewGroup名为SlideDetailsLayout
SlideDetailsLayout内容有两个View, mFrontView(第一个View)和mBehindView(第二个View)
有两种状态, 状态设置为close就显示第一个商品数据View, open状态就显示第二个图文详情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<SavedState> CREATOR =
new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}


github地址
apk下载
这个商品详情页的架构也是本人在已上线的项目中使用
觉得实用的star一下, 谢谢大家


最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台