Android仿京东、天猫商品详情页
前言
前面在介绍控件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); } } @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等的页面填充,也没啥好讲的,代码又很多可以优化的地方,在优化的地方,笔者也列出了优化的方案,大家可以根据自己的实际情况做页面级的优化。
源码下载:http://xiazai.jb51.net/201701/yuanma/andriodgooddetail(jb51.net).rar
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: Android中获取资源 id 及资源 id 的动态获取
下一篇: java数独游戏完整版分享