Android仿京東、天貓商品詳情頁(yè)
前言
前面在介紹控件TabLayout控件和CoordinatorLayout使用的時(shí)候說了下實(shí)現(xiàn)京東、天貓?jiān)斍轫?yè)面的效果,今天要說的是優(yōu)化版,是我們線上實(shí)現(xiàn)的效果,首先看一張效果:
項(xiàng)目結(jié)構(gòu)分析
首先我們來分析一下要實(shí)現(xiàn)上面的效果,我們需要怎么做。頂部是一個(gè)可以滑動(dòng)切換Tab,可以用ViewPager+Fragment實(shí)現(xiàn),也可以使用系統(tǒng)的TabLayout控件實(shí)現(xiàn);而下面的 View是一個(gè)可以滑動(dòng)拖動(dòng)效果的View,可以采用網(wǎng)上一個(gè)叫做DragLayout的控件,我這里是自己實(shí)現(xiàn)了一個(gè),主要是通過對(duì)View的事件分發(fā)的一些處理;然后滑動(dòng)到下面就是一個(gè)圖文詳情的View(Fragment),本頁(yè)面包含兩個(gè)界面:詳情頁(yè)面和參數(shù)頁(yè)面;最后是評(píng)價(jià)的View(Fragment)。經(jīng)過上面的分析,我們的界面至少需要4個(gè)Fragement,首先來看一下項(xiàng)目結(jié)構(gòu):
代碼講解
代碼比較多,這里只講解幾個(gè)核心的方法類。首先我們來看一下我們自己是的這個(gè)具有阻尼效果的View,我們知道要實(shí)現(xiàn)的效果,我們需要對(duì)View的事件做一個(gè)全面的實(shí)現(xiàn)。這里首先說一下View的事件分發(fā)的流程:
onInterceptTouchEvent()–>dispatchTouchEvent()–>onTouchEvent();
首先我們需要對(duì)View傳過來的事件做一個(gè)攔截:
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;
最后轉(zhuǎn)發(fā)給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;
滑動(dòng)事件完了之后我們需要調(diào)用request方法對(duì)View做一個(gè)重繪:
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); }
上下滑動(dòng)也是涉及到兩個(gè)界面:mFrontView和mBehindView,然后通過判斷滑動(dòng)事件來顯示哪一個(gè)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等的頁(yè)面填充,也沒啥好講的,代碼又很多可以優(yōu)化的地方,在優(yōu)化的地方,筆者也列出了優(yōu)化的方案,大家可以根據(jù)自己的實(shí)際情況做頁(yè)面級(jí)的優(yōu)化。
源碼下載:http://xiazai.jb51.net/201701/yuanma/AndriodGoodDetail(jb51.net).rar
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android 仿京東秒殺倒計(jì)時(shí)代碼
- Android 仿淘寶、京東商品詳情頁(yè)向上拖動(dòng)查看圖文詳情控件DEMO詳解
- Android中使用TextView實(shí)現(xiàn)高仿京東淘寶各種倒計(jì)時(shí)效果
- Android 仿京東、拼多多商品分類頁(yè)的示例代碼
- Android高仿京東垂直循環(huán)滾動(dòng)新聞欄
- Android 仿京東側(cè)滑篩選實(shí)例代碼
- Android仿支付寶、京東的密碼鍵盤和輸入框
- Android仿京東、天貓下拉刷新效果
- android仿京東商品屬性篩選功能
- Android實(shí)現(xiàn)京東秒殺界面
相關(guān)文章
Android NDK 開發(fā)中 SO 包大小壓縮方法詳解
這篇文章主要為為大家介紹了Android NDK 開發(fā)中 SO 包大小壓縮方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09詳解Android短信的發(fā)送和廣播接收實(shí)現(xiàn)短信的監(jiān)聽
本篇文章主要介紹了Android短信的發(fā)送和廣播接收實(shí)現(xiàn)短信的監(jiān)聽,可以實(shí)現(xiàn)短信收發(fā),有興趣的可以了解一下。2016-11-11Android 異步任務(wù)和消息機(jī)制面試題分析
這篇文章主要為大家介紹了Android 異步任務(wù)和消息機(jī)制面試題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07Android設(shè)置TextView首行縮進(jìn)示例代碼
使用過word的都會(huì)知道,在文字排版的時(shí)候經(jīng)常要設(shè)置首行縮進(jìn),這樣才會(huì)使排版更整齊,那么在Android中當(dāng)需要設(shè)置首行縮進(jìn)的時(shí)候該腫么辦呢,下面一起來看看。2016-08-08Android開發(fā)實(shí)現(xiàn)自定義Toast、LayoutInflater使用其他布局示例
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)自定義Toast、LayoutInflater使用其他布局,涉及Android自定義Toast與界面布局相關(guān)操作技巧,需要的朋友可以參考下2019-03-03Android 應(yīng)用中插入廣告詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android 應(yīng)用中插入廣告詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2016-10-10Android DrawerLayout實(shí)現(xiàn)側(cè)拉菜單功能
這篇文章主要介紹了Android DrawerLayout實(shí)現(xiàn)側(cè)拉菜單功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-06-06android 幀動(dòng)畫,補(bǔ)間動(dòng)畫,屬性動(dòng)畫的簡(jiǎn)單總結(jié)
本文主要對(duì)android 幀動(dòng)畫,補(bǔ)間動(dòng)畫,屬性動(dòng)畫進(jìn)行了簡(jiǎn)單總結(jié),具有一定的參考價(jià)值,下面跟著小編一起來看下吧2017-01-01