zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

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

Android手机开发 详解 京东 商品 天猫 详情页
2023-06-13 09:20:14 时间

前面在介绍控件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); 


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仿京东、天猫商品详情页源码

5874.html

app程序应用开发手机开发无线开发移动端开发