完美解决关于禁止ViewPager预加载的相关问题
我最近上班又遇到一个小难题了,就是如题所述:viewpager预加载的问题。相信用过viewpager的人大抵都有遇到过这种情况,网上的解决办法也就那么几个,终于在我自己不断试验之下,完美解决了(禁止了)viewpager的预加载。
好了,首先来说明一下,什么是viewpager的预加载:viewpager有一个 “预加载”的机制,默认会把viewpager当前位置的左右相邻页面预先初始化(俗称的预加载),它的默认值是 1,这样做的好处就是viewpager左右滑动会更加流畅。
可是我的情况很特殊,因为我 5 个fragment里有一个fragment是有surfaceview的,这样造成的问题就是,我viewpager滑动到其相邻页面时,含有surfaceview的页面就会被预先初始化,然后surfaceview就开始预览了,只是我们看不到而已。同样的,当我们从含有surfaceview的页面滑倒其相邻的页面时,surfaceview并不会回调其surfacedestory方法。于是这给我造成了极大的困扰。
ok,下面言归正传,到底该怎么禁止viewpager的这个预加载问题呢?
方案1:网上大多数说法是 懒加载,即让viewpager预加载初始化ui,而具体一些数据,网络访问请求等延迟加载。这是靠fragment里有一个setuservisiblehint(boolean isvisibletouser)的方法,我们可以在这个方法里做判断,当其true可见时(即切换到某一个具体fragment)时才去加载数据,这样可以省流量。但这里并不满足我的需求,因为某一个fragment并不会在viewpager滑动到其相邻的fragment时销毁。这个只可以解决部分人问题。
首先我们来深入了解下viewpager的预加载机制:
上文提到过,viewpager默认预加载的数量是1,这一点我们可以在viewpager源码里看到。
default_offscreen_pages 这里就定义了默认值是1, 所以网上 有种解决方案 说调用viewpager的setoffscreenpagelimit(int limit),来设置viewpager预加载的数量,但是这里很明确的告诉你,这种方案是不可行的,如下图viewpager源码:
我们可以看到,如果你调用该方法传进来的值小于1是无效的,会被强行的拽回1。而且default_offscreen_pages 这个值是private的,子类继承viewpager也是不可见的。
方案2:然后网上有第二种说法,自定义一个viewpager,把原生viewpager全拷过来,修改这个default_offscreen_pages 值为0。对,就是这种解决方案!!但是!!
但是!!!但是什么呢?但是我试过,没用。为什么呢?接下来就是本文的重点了。
因为现在android都6.0了,版本都老高了,其实android虽然每个版本都有v4包,但是这些v4包是有差异的。这就造成高版本v4包里的viewpager,即使你copy它,将其default_offscreen_pages的值改为0,还是不起作用的,其中的逻辑变了。具体哪里变了导致无效我也没有继续研究了,毕竟公司不会花时间让我研究这些啊。偷笑
完美解决方案:ok,所以关于禁止viewpager预加载的完美解决方案就是,使用低版本v4包里的viewpager,完全copy一份,将其中的default_offscreen_pages值改为0即可。博主亲测 api 14 即 android 4.0的v4包里viewpager 有效。
当然,谷歌既然有这么一种viewpager的机制肯定有它的道理,所以一般还是预加载的好。
最后,因为低版本的源码越来越少的人会去下载,这里直接把这个禁止了预加载的viewpager贴上来,需要的人就拿去吧。copy就能用了。
package com.plumcot.usb.view; /* * copyright (c) 2011 the android open source project * * licensed under the apache license, version 2.0 (the "license"); * you may not use this file except in compliance with the license. * you may obtain a copy of the license at * * http://www.apache.org/licenses/license-2.0 * * unless required by applicable law or agreed to in writing, software * distributed under the license is distributed on an "as is" basis, * without warranties or conditions of any kind, either express or implied. * see the license for the specific language governing permissions and * limitations under the license. */ import android.content.context; import android.database.datasetobserver; import android.graphics.canvas; import android.graphics.rect; import android.graphics.drawable.drawable; import android.os.parcel; import android.os.parcelable; import android.os.systemclock; import android.support.v4.os.parcelablecompat; import android.support.v4.os.parcelablecompatcreatorcallbacks; import android.support.v4.view.keyeventcompat; import android.support.v4.view.motioneventcompat; import android.support.v4.view.pageradapter; import android.support.v4.view.velocitytrackercompat; import android.support.v4.view.viewcompat; import android.support.v4.view.viewconfigurationcompat; import android.support.v4.widget.edgeeffectcompat; import android.util.attributeset; import android.util.log; import android.view.focusfinder; import android.view.keyevent; import android.view.motionevent; import android.view.soundeffectconstants; import android.view.velocitytracker; import android.view.view; import android.view.viewconfiguration; import android.view.viewgroup; import android.view.viewparent; import android.view.accessibility.accessibilityevent; import android.view.animation.interpolator; import android.widget.scroller; import java.util.arraylist; import java.util.collections; import java.util.comparator; /** * layout manager that allows the user to flip left and right * through pages of data. you supply an implementation of a * {@link android.support.v4.view.pageradapter} to generate the pages that the view shows. * * <p>note this class is currently under early design and * development. the api will likely change in later updates of * the compatibility library, requiring changes to the source code * of apps when they are compiled against the newer version.</p> */ public class nopreloadviewpager extends viewgroup { private static final string tag = "<span style="font-family:arial, helvetica, sans-serif;">nopreloadviewpager</span>"; private static final boolean debug = false; private static final boolean use_cache = false; private static final int default_offscreen_pages = 0;//默认是1 private static final int max_settle_duration = 600; // ms static class iteminfo { object object; int position; boolean scrolling; } private static final comparator<iteminfo> comparator = new comparator<iteminfo>(){ @override public int compare(iteminfo lhs, iteminfo rhs) { return lhs.position - rhs.position; }}; private static final interpolator sinterpolator = new interpolator() { public float getinterpolation(float t) { // _o(t) = t * t * ((tension + 1) * t + tension) // o(t) = _o(t - 1) + 1 t -= 1.0f; return t * t * t + 1.0f; } }; private final arraylist<iteminfo> mitems = new arraylist<iteminfo>(); private pageradapter madapter; private int mcuritem; // index of currently displayed page. private int mrestoredcuritem = -1; private parcelable mrestoredadapterstate = null; private classloader mrestoredclassloader = null; private scroller mscroller; private pagerobserver mobserver; private int mpagemargin; private drawable mmargindrawable; private int mchildwidthmeasurespec; private int mchildheightmeasurespec; private boolean minlayout; private boolean mscrollingcacheenabled; private boolean mpopulatepending; private boolean mscrolling; private int moffscreenpagelimit = default_offscreen_pages; private boolean misbeingdragged; private boolean misunabletodrag; private int mtouchslop; private float minitialmotionx; /** * position of the last motion event. */ private float mlastmotionx; private float mlastmotiony; /** * id of the active pointer. this is used to retain consistency during * drags/flings if multiple pointers are used. */ private int mactivepointerid = invalid_pointer; /** * sentinel value for no current active pointer. * used by {@link #mactivepointerid}. */ private static final int invalid_pointer = -1; /** * determines speed during touch scrolling */ private velocitytracker mvelocitytracker; private int mminimumvelocity; private int mmaximumvelocity; private float mbaselineflingvelocity; private float mflingvelocityinfluence; private boolean mfakedragging; private long mfakedragbegintime; private edgeeffectcompat mleftedge; private edgeeffectcompat mrightedge; private boolean mfirstlayout = true; private onpagechangelistener monpagechangelistener; /** * indicates that the pager is in an idle, settled state. the current page * is fully in view and no animation is in progress. */ public static final int scroll_state_idle = 0; /** * indicates that the pager is currently being dragged by the user. */ public static final int scroll_state_dragging = 1; /** * indicates that the pager is in the process of settling to a final position. */ public static final int scroll_state_settling = 2; private int mscrollstate = scroll_state_idle; /** * callback interface for responding to changing state of the selected page. */ public interface onpagechangelistener { /** * this method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * * @param position position index of the first page currently being displayed. * page position+1 will be visible if positionoffset is nonzero. * @param positionoffset value from [0, 1) indicating the offset from the page at position. * @param positionoffsetpixels value in pixels indicating the offset from position. */ public void onpagescrolled(int position, float positionoffset, int positionoffsetpixels); /** * this method will be invoked when a new page becomes selected. animation is not * necessarily complete. * * @param position position index of the new selected page. */ public void onpageselected(int position); /** * called when the scroll state changes. useful for discovering when the user * begins dragging, when the pager is automatically settling to the current page, * or when it is fully stopped/idle. * * @param state the new scroll state. * @see android.support.v4.view.viewpager#scroll_state_idle * @see android.support.v4.view.viewpager#scroll_state_dragging * @see android.support.v4.view.viewpager#scroll_state_settling */ public void onpagescrollstatechanged(int state); } /** * simple implementation of the {@link android.support.v4.view.lazyviewpager.onpagechangelistener} interface with stub * implementations of each method. extend this if you do not intend to override * every method of {@link android.support.v4.view.lazyviewpager.onpagechangelistener}. */ public static class simpleonpagechangelistener implements onpagechangelistener { @override public void onpagescrolled(int position, float positionoffset, int positionoffsetpixels) { // this space for rent } @override public void onpageselected(int position) { // this space for rent } @override public void onpagescrollstatechanged(int state) { // this space for rent } } public nopreloadviewpager(context context) { super(context); initviewpager(); } public nopreloadviewpager(context context, attributeset attrs) { super(context, attrs); initviewpager(); } void initviewpager() { setwillnotdraw(false); setdescendantfocusability(focus_after_descendants); setfocusable(true); final context context = getcontext(); mscroller = new scroller(context, sinterpolator); final viewconfiguration configuration = viewconfiguration.get(context); mtouchslop = viewconfigurationcompat.getscaledpagingtouchslop(configuration); mminimumvelocity = configuration.getscaledminimumflingvelocity(); mmaximumvelocity = configuration.getscaledmaximumflingvelocity(); mleftedge = new edgeeffectcompat(context); mrightedge = new edgeeffectcompat(context); float density = context.getresources().getdisplaymetrics().density; mbaselineflingvelocity = 2500.0f * density; mflingvelocityinfluence = 0.4f; } private void setscrollstate(int newstate) { if (mscrollstate == newstate) { return; } mscrollstate = newstate; if (monpagechangelistener != null) { monpagechangelistener.onpagescrollstatechanged(newstate); } } public void setadapter(pageradapter adapter) { if (madapter != null) { // madapter.unregisterdatasetobserver(mobserver); madapter.startupdate(this); for (int i = 0; i < mitems.size(); i++) { final iteminfo ii = mitems.get(i); madapter.destroyitem(this, ii.position, ii.object); } madapter.finishupdate(this); mitems.clear(); removeallviews(); mcuritem = 0; scrollto(0, 0); } madapter = adapter; if (madapter != null) { if (mobserver == null) { mobserver = new pagerobserver(); } // madapter.registerdatasetobserver(mobserver); mpopulatepending = false; if (mrestoredcuritem >= 0) { madapter.restorestate(mrestoredadapterstate, mrestoredclassloader); setcurrentiteminternal(mrestoredcuritem, false, true); mrestoredcuritem = -1; mrestoredadapterstate = null; mrestoredclassloader = null; } else { populate(); } } } public pageradapter getadapter() { return madapter; } /** * set the currently selected page. if the viewpager has already been through its first * layout there will be a smooth animated transition between the current item and the * specified item. * * @param item item index to select */ public void setcurrentitem(int item) { mpopulatepending = false; setcurrentiteminternal(item, !mfirstlayout, false); } /** * set the currently selected page. * * @param item item index to select * @param smoothscroll true to smoothly scroll to the new item, false to transition immediately */ public void setcurrentitem(int item, boolean smoothscroll) { mpopulatepending = false; setcurrentiteminternal(item, smoothscroll, false); } public int getcurrentitem() { return mcuritem; } void setcurrentiteminternal(int item, boolean smoothscroll, boolean always) { setcurrentiteminternal(item, smoothscroll, always, 0); } void setcurrentiteminternal(int item, boolean smoothscroll, boolean always, int velocity) { if (madapter == null || madapter.getcount() <= 0) { setscrollingcacheenabled(false); return; } if (!always && mcuritem == item && mitems.size() != 0) { setscrollingcacheenabled(false); return; } if (item < 0) { item = 0; } else if (item >= madapter.getcount()) { item = madapter.getcount() - 1; } final int pagelimit = moffscreenpagelimit; if (item > (mcuritem + pagelimit) || item < (mcuritem - pagelimit)) { // we are doing a jump by more than one page. to avoid // glitches, we want to keep all current pages in the view // until the scroll ends. for (int i=0; i<mitems.size(); i++) { mitems.get(i).scrolling = true; } } final boolean dispatchselected = mcuritem != item; mcuritem = item; populate(); final int destx = (getwidth() + mpagemargin) * item; if (smoothscroll) { smoothscrollto(destx, 0, velocity); if (dispatchselected && monpagechangelistener != null) { monpagechangelistener.onpageselected(item); } } else { if (dispatchselected && monpagechangelistener != null) { monpagechangelistener.onpageselected(item); } completescroll(); scrollto(destx, 0); } } public void setonpagechangelistener(onpagechangelistener listener) { monpagechangelistener = listener; } /** * returns the number of pages that will be retained to either side of the * current page in the view hierarchy in an idle state. defaults to 1. * * @return how many pages will be kept offscreen on either side * @see #setoffscreenpagelimit(int) */ public int getoffscreenpagelimit() { return moffscreenpagelimit; } /** * set the number of pages that should be retained to either side of the * current page in the view hierarchy in an idle state. pages beyond this * limit will be recreated from the adapter when needed. * * <p>this is offered as an optimization. if you know in advance the number * of pages you will need to support or have lazy-loading mechanisms in place * on your pages, tweaking this setting can have benefits in perceived smoothness * of paging animations and interaction. if you have a small number of pages (3-4) * that you can keep active all at once, less time will be spent in layout for * newly created view subtrees as the user pages back and forth.</p> * * <p>you should keep this limit low, especially if your pages have complex layouts. * this setting defaults to 1.</p> * * @param limit how many pages will be kept offscreen in an idle state. */ public void setoffscreenpagelimit(int limit) { if (limit < default_offscreen_pages) { log.w(tag, "requested offscreen page limit " + limit + " too small; defaulting to " + default_offscreen_pages); limit = default_offscreen_pages; } if (limit != moffscreenpagelimit) { moffscreenpagelimit = limit; populate(); } } /** * set the margin between pages. * * @param marginpixels distance between adjacent pages in pixels * @see #getpagemargin() * @see #setpagemargindrawable(android.graphics.drawable.drawable) * @see #setpagemargindrawable(int) */ public void setpagemargin(int marginpixels) { final int oldmargin = mpagemargin; mpagemargin = marginpixels; final int width = getwidth(); recomputescrollposition(width, width, marginpixels, oldmargin); requestlayout(); } /** * return the margin between pages. * * @return the size of the margin in pixels */ public int getpagemargin() { return mpagemargin; } /** * set a drawable that will be used to fill the margin between pages. * * @param d drawable to display between pages */ public void setpagemargindrawable(drawable d) { mmargindrawable = d; if (d != null) refreshdrawablestate(); setwillnotdraw(d == null); invalidate(); } /** * set a drawable that will be used to fill the margin between pages. * * @param resid resource id of a drawable to display between pages */ public void setpagemargindrawable(int resid) { setpagemargindrawable(getcontext().getresources().getdrawable(resid)); } @override protected boolean verifydrawable(drawable who) { return super.verifydrawable(who) || who == mmargindrawable; } @override protected void drawablestatechanged() { super.drawablestatechanged(); final drawable d = mmargindrawable; if (d != null && d.isstateful()) { d.setstate(getdrawablestate()); } } // we want the duration of the page snap animation to be influenced by the distance that // the screen has to travel, however, we don't want this duration to be effected in a // purely linear fashion. instead, we use this method to moderate the effect that the distance // of travel has on the overall snap duration. float distanceinfluenceforsnapduration(float f) { f -= 0.5f; // center the values about 0. f *= 0.3f * math.pi / 2.0f; return (float) math.sin(f); } /** * like {@link android.view.view#scrollby}, but scroll smoothly instead of immediately. * * @param x the number of pixels to scroll by on the x axis * @param y the number of pixels to scroll by on the y axis */ void smoothscrollto(int x, int y) { smoothscrollto(x, y, 0); } /** * like {@link android.view.view#scrollby}, but scroll smoothly instead of immediately. * * @param x the number of pixels to scroll by on the x axis * @param y the number of pixels to scroll by on the y axis * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) */ void smoothscrollto(int x, int y, int velocity) { if (getchildcount() == 0) { // nothing to do. setscrollingcacheenabled(false); return; } int sx = getscrollx(); int sy = getscrolly(); int dx = x - sx; int dy = y - sy; if (dx == 0 && dy == 0) { completescroll(); setscrollstate(scroll_state_idle); return; } setscrollingcacheenabled(true); mscrolling = true; setscrollstate(scroll_state_settling); final float pagedelta = (float) math.abs(dx) / (getwidth() + mpagemargin); int duration = (int) (pagedelta * 100); velocity = math.abs(velocity); if (velocity > 0) { duration += (duration / (velocity / mbaselineflingvelocity)) * mflingvelocityinfluence; } else { duration += 100; } duration = math.min(duration, max_settle_duration); mscroller.startscroll(sx, sy, dx, dy, duration); invalidate(); } void addnewitem(int position, int index) { iteminfo ii = new iteminfo(); ii.position = position; ii.object = madapter.instantiateitem(this, position); if (index < 0) { mitems.add(ii); } else { mitems.add(index, ii); } } void datasetchanged() { // this method only gets called if our observer is attached, so madapter is non-null. boolean needpopulate = mitems.size() < 3 && mitems.size() < madapter.getcount(); int newcurritem = -1; for (int i = 0; i < mitems.size(); i++) { final iteminfo ii = mitems.get(i); final int newpos = madapter.getitemposition(ii.object); if (newpos == pageradapter.position_unchanged) { continue; } if (newpos == pageradapter.position_none) { mitems.remove(i); i--; madapter.destroyitem(this, ii.position, ii.object); needpopulate = true; if (mcuritem == ii.position) { // keep the current item in the valid range newcurritem = math.max(0, math.min(mcuritem, madapter.getcount() - 1)); } continue; } if (ii.position != newpos) { if (ii.position == mcuritem) { // our current item changed position. follow it. newcurritem = newpos; } ii.position = newpos; needpopulate = true; } } collections.sort(mitems, comparator); if (newcurritem >= 0) { // todo this currently causes a jump. setcurrentiteminternal(newcurritem, false, true); needpopulate = true; } if (needpopulate) { populate(); requestlayout(); } } void populate() { if (madapter == null) { return; } // bail now if we are waiting to populate. this is to hold off // on creating views from the time the user releases their finger to // fling to a new position until we have finished the scroll to // that position, avoiding glitches from happening at that point. if (mpopulatepending) { if (debug) log.i(tag, "populate is pending, skipping for now..."); return; } // also, don't populate until we are attached to a window. this is to // avoid trying to populate before we have restored our view hierarchy // state and conflicting with what is restored. if (getwindowtoken() == null) { return; } madapter.startupdate(this); final int pagelimit = moffscreenpagelimit; final int startpos = math.max(0, mcuritem - pagelimit); final int n = madapter.getcount(); final int endpos = math.min(n-1, mcuritem + pagelimit); if (debug) log.v(tag, "populating: startpos=" + startpos + " endpos=" + endpos); // add and remove pages in the existing list. int lastpos = -1; for (int i=0; i<mitems.size(); i++) { iteminfo ii = mitems.get(i); if ((ii.position < startpos || ii.position > endpos) && !ii.scrolling) { if (debug) log.i(tag, "removing: " + ii.position + " @ " + i); mitems.remove(i); i--; madapter.destroyitem(this, ii.position, ii.object); } else if (lastpos < endpos && ii.position > startpos) { // the next item is outside of our range, but we have a gap // between it and the last item where we want to have a page // shown. fill in the gap. lastpos++; if (lastpos < startpos) { lastpos = startpos; } while (lastpos <= endpos && lastpos < ii.position) { if (debug) log.i(tag, "inserting: " + lastpos + " @ " + i); addnewitem(lastpos, i); lastpos++; i++; } } lastpos = ii.position; } // add any new pages we need at the end. lastpos = mitems.size() > 0 ? mitems.get(mitems.size()-1).position : -1; if (lastpos < endpos) { lastpos++; lastpos = lastpos > startpos ? lastpos : startpos; while (lastpos <= endpos) { if (debug) log.i(tag, "appending: " + lastpos); addnewitem(lastpos, -1); lastpos++; } } if (debug) { log.i(tag, "current page list:"); for (int i=0; i<mitems.size(); i++) { log.i(tag, "#" + i + ": page " + mitems.get(i).position); } } iteminfo curitem = null; for (int i=0; i<mitems.size(); i++) { if (mitems.get(i).position == mcuritem) { curitem = mitems.get(i); break; } } madapter.setprimaryitem(this, mcuritem, curitem != null ? curitem.object : null); madapter.finishupdate(this); if (hasfocus()) { view currentfocused = findfocus(); iteminfo ii = currentfocused != null ? infoforanychild(currentfocused) : null; if (ii == null || ii.position != mcuritem) { for (int i=0; i<getchildcount(); i++) { view child = getchildat(i); ii = infoforchild(child); if (ii != null && ii.position == mcuritem) { if (child.requestfocus(focus_forward)) { break; } } } } } } public static class savedstate extends basesavedstate { int position; parcelable adapterstate; classloader loader; public savedstate(parcelable superstate) { super(superstate); } @override public void writetoparcel(parcel out, int flags) { super.writetoparcel(out, flags); out.writeint(position); out.writeparcelable(adapterstate, flags); } @override public string tostring() { return "fragmentpager.savedstate{" + integer.tohexstring(system.identityhashcode(this)) + " position=" + position + "}"; } public static final creator<savedstate> creator = parcelablecompat.newcreator(new parcelablecompatcreatorcallbacks<savedstate>() { @override public savedstate createfromparcel(parcel in, classloader loader) { return new savedstate(in, loader); } @override public savedstate[] newarray(int size) { return new savedstate[size]; } }); savedstate(parcel in, classloader loader) { super(in); if (loader == null) { loader = getclass().getclassloader(); } position = in.readint(); adapterstate = in.readparcelable(loader); this.loader = loader; } } @override public parcelable onsaveinstancestate() { parcelable superstate = super.onsaveinstancestate(); savedstate ss = new savedstate(superstate); ss.position = mcuritem; if (madapter != null) { ss.adapterstate = madapter.savestate(); } return ss; } @override public void onrestoreinstancestate(parcelable state) { if (!(state instanceof savedstate)) { super.onrestoreinstancestate(state); return; } savedstate ss = (savedstate)state; super.onrestoreinstancestate(ss.getsuperstate()); if (madapter != null) { madapter.restorestate(ss.adapterstate, ss.loader); setcurrentiteminternal(ss.position, false, true); } else { mrestoredcuritem = ss.position; mrestoredadapterstate = ss.adapterstate; mrestoredclassloader = ss.loader; } } @override public void addview(view child, int index, layoutparams params) { if (minlayout) { addviewinlayout(child, index, params); child.measure(mchildwidthmeasurespec, mchildheightmeasurespec); } else { super.addview(child, index, params); } if (use_cache) { if (child.getvisibility() != gone) { child.setdrawingcacheenabled(mscrollingcacheenabled); } else { child.setdrawingcacheenabled(false); } } } iteminfo infoforchild(view child) { for (int i=0; i<mitems.size(); i++) { iteminfo ii = mitems.get(i); if (madapter.isviewfromobject(child, ii.object)) { return ii; } } return null; } iteminfo infoforanychild(view child) { viewparent parent; while ((parent=child.getparent()) != this) { if (parent == null || !(parent instanceof view)) { return null; } child = (view)parent; } return infoforchild(child); } @override protected void onattachedtowindow() { super.onattachedtowindow(); mfirstlayout = true; } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { // for simple implementation, or internal size is always 0. // we depend on the container to specify the layout size of // our view. we can't really know what it is since we will be // adding and removing different arbitrary views and do not // want the layout to change as this happens. setmeasureddimension(getdefaultsize(0, widthmeasurespec), getdefaultsize(0, heightmeasurespec)); // children are just made to fill our space. mchildwidthmeasurespec = measurespec.makemeasurespec(getmeasuredwidth() - getpaddingleft() - getpaddingright(), measurespec.exactly); mchildheightmeasurespec = measurespec.makemeasurespec(getmeasuredheight() - getpaddingtop() - getpaddingbottom(), measurespec.exactly); // make sure we have created all fragments that we need to have shown. minlayout = true; populate(); minlayout = false; // make sure all children have been properly measured. final int size = getchildcount(); for (int i = 0; i < size; ++i) { final view child = getchildat(i); if (child.getvisibility() != gone) { if (debug) log.v(tag, "measuring #" + i + " " + child + ": " + mchildwidthmeasurespec); child.measure(mchildwidthmeasurespec, mchildheightmeasurespec); } } } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); // make sure scroll position is set correctly. if (w != oldw) { recomputescrollposition(w, oldw, mpagemargin, mpagemargin); } } private void recomputescrollposition(int width, int oldwidth, int margin, int oldmargin) { final int widthwithmargin = width + margin; if (oldwidth > 0) { final int oldscrollpos = getscrollx(); final int oldwwm = oldwidth + oldmargin; final int oldscrollitem = oldscrollpos / oldwwm; final float scrolloffset = (float) (oldscrollpos % oldwwm) / oldwwm; final int scrollpos = (int) ((oldscrollitem + scrolloffset) * widthwithmargin); scrollto(scrollpos, getscrolly()); if (!mscroller.isfinished()) { // we now return to your regularly scheduled scroll, already in progress. final int newduration = mscroller.getduration() - mscroller.timepassed(); mscroller.startscroll(scrollpos, 0, mcuritem * widthwithmargin, 0, newduration); } } else { int scrollpos = mcuritem * widthwithmargin; if (scrollpos != getscrollx()) { completescroll(); scrollto(scrollpos, getscrolly()); } } } @override protected void onlayout(boolean changed, int l, int t, int r, int b) { minlayout = true; populate(); minlayout = false; final int count = getchildcount(); final int width = r-l; for (int i = 0; i < count; i++) { view child = getchildat(i); iteminfo ii; if (child.getvisibility() != gone && (ii=infoforchild(child)) != null) { int loff = (width + mpagemargin) * ii.position; int childleft = getpaddingleft() + loff; int childtop = getpaddingtop(); if (debug) log.v(tag, "positioning #" + i + " " + child + " f=" + ii.object + ":" + childleft + "," + childtop + " " + child.getmeasuredwidth() + "x" + child.getmeasuredheight()); child.layout(childleft, childtop, childleft + child.getmeasuredwidth(), childtop + child.getmeasuredheight()); } } mfirstlayout = false; } @override public void computescroll() { if (debug) log.i(tag, "computescroll: finished=" + mscroller.isfinished()); if (!mscroller.isfinished()) { if (mscroller.computescrolloffset()) { if (debug) log.i(tag, "computescroll: still scrolling"); int oldx = getscrollx(); int oldy = getscrolly(); int x = mscroller.getcurrx(); int y = mscroller.getcurry(); if (oldx != x || oldy != y) { scrollto(x, y); } if (monpagechangelistener != null) { final int widthwithmargin = getwidth() + mpagemargin; final int position = x / widthwithmargin; final int offsetpixels = x % widthwithmargin; final float offset = (float) offsetpixels / widthwithmargin; monpagechangelistener.onpagescrolled(position, offset, offsetpixels); } // keep on drawing until the animation has finished. invalidate(); return; } } // done with scroll, clean up state. completescroll(); } private void completescroll() { boolean needpopulate = mscrolling; if (needpopulate) { // done with scroll, no longer want to cache view drawing. setscrollingcacheenabled(false); mscroller.abortanimation(); int oldx = getscrollx(); int oldy = getscrolly(); int x = mscroller.getcurrx(); int y = mscroller.getcurry(); if (oldx != x || oldy != y) { scrollto(x, y); } setscrollstate(scroll_state_idle); } mpopulatepending = false; mscrolling = false; for (int i=0; i<mitems.size(); i++) { iteminfo ii = mitems.get(i); if (ii.scrolling) { needpopulate = true; ii.scrolling = false; } } if (needpopulate) { populate(); } } @override public boolean onintercepttouchevent(motionevent ev) { /* * this method just determines whether we want to intercept the motion. * if we return true, onmotionevent will be called and we do the actual * scrolling there. */ final int action = ev.getaction() & motioneventcompat.action_mask; // always take care of the touch gesture being complete. if (action == motionevent.action_cancel || action == motionevent.action_up) { // release the drag. if (debug) log.v(tag, "intercept done!"); misbeingdragged = false; misunabletodrag = false; mactivepointerid = invalid_pointer; return false; } // nothing more to do here if we have decided whether or not we // are dragging. if (action != motionevent.action_down) { if (misbeingdragged) { if (debug) log.v(tag, "intercept returning true!"); return true; } if (misunabletodrag) { if (debug) log.v(tag, "intercept returning false!"); return false; } } switch (action) { case motionevent.action_move: { /* * misbeingdragged == false, otherwise the shortcut would have caught it. check * whether the user has moved far enough from his original down touch. */ /* * locally do absolute value. mlastmotiony is set to the y value * of the down event. */ final int activepointerid = mactivepointerid; if (activepointerid == invalid_pointer) { // if we don't have a valid id, the touch down wasn't on content. break; } final int pointerindex = motioneventcompat.findpointerindex(ev, activepointerid); final float x = motioneventcompat.getx(ev, pointerindex); final float dx = x - mlastmotionx; final float xdiff = math.abs(dx); final float y = motioneventcompat.gety(ev, pointerindex); final float ydiff = math.abs(y - mlastmotiony); final int scrollx = getscrollx(); final boolean atedge = (dx > 0 && scrollx == 0) || (dx < 0 && madapter != null && scrollx >= (madapter.getcount() - 1) * getwidth() - 1); if (debug) log.v(tag, "moved x to " + x + "," + y + " diff=" + xdiff + "," + ydiff); if (canscroll(this, false, (int) dx, (int) x, (int) y)) { // nested view has scrollable area under this point. let it be handled there. minitialmotionx = mlastmotionx = x; mlastmotiony = y; return false; } if (xdiff > mtouchslop && xdiff > ydiff) { if (debug) log.v(tag, "starting drag!"); misbeingdragged = true; setscrollstate(scroll_state_dragging); mlastmotionx = x; setscrollingcacheenabled(true); } else { if (ydiff > mtouchslop) { // the finger has moved enough in the vertical // direction to be counted as a drag... abort // any attempt to drag horizontally, to work correctly // with children that have scrolling containers. if (debug) log.v(tag, "starting unable to drag!"); misunabletodrag = true; } } break; } case motionevent.action_down: { /* * remember location of down touch. * action_down always refers to pointer index 0. */ mlastmotionx = minitialmotionx = ev.getx(); mlastmotiony = ev.gety(); mactivepointerid = motioneventcompat.getpointerid(ev, 0); if (mscrollstate == scroll_state_settling) { // let the user 'catch' the pager as it animates. misbeingdragged = true; misunabletodrag = false; setscrollstate(scroll_state_dragging); } else { completescroll(); misbeingdragged = false; misunabletodrag = false; } if (debug) log.v(tag, "down at " + mlastmotionx + "," + mlastmotiony + " misbeingdragged=" + misbeingdragged + "misunabletodrag=" + misunabletodrag); break; } case motioneventcompat.action_pointer_up: onsecondarypointerup(ev); break; } /* * the only time we want to intercept motion events is if we are in the * drag mode. */ return misbeingdragged; } @override public boolean ontouchevent(motionevent ev) { if (mfakedragging) { // a fake drag is in progress already, ignore this real one // but still eat the touch events. // (it is likely that the user is multi-touching the screen.) return true; } if (ev.getaction() == motionevent.action_down && ev.getedgeflags() != 0) { // don't handle edge touches immediately -- they may actually belong to one of our // descendants. return false; } if (madapter == null || madapter.getcount() == 0) { // nothing to present or scroll; nothing to touch. return false; } if (mvelocitytracker == null) { mvelocitytracker = velocitytracker.obtain(); } mvelocitytracker.addmovement(ev); final int action = ev.getaction(); boolean needsinvalidate = false; switch (action & motioneventcompat.action_mask) { case motionevent.action_down: { /* * if being flinged and user touches, stop the fling. isfinished * will be false if being flinged. */ completescroll(); // remember where the motion event started mlastmotionx = minitialmotionx = ev.getx(); mactivepointerid = motioneventcompat.getpointerid(ev, 0); break; } case motionevent.action_move: if (!misbeingdragged) { final int pointerindex = motioneventcompat.findpointerindex(ev, mactivepointerid); final float x = motioneventcompat.getx(ev, pointerindex); final float xdiff = math.abs(x - mlastmotionx); final float y = motioneventcompat.gety(ev, pointerindex); final float ydiff = math.abs(y - mlastmotiony); if (debug) log.v(tag, "moved x to " + x + "," + y + " diff=" + xdiff + "," + ydiff); if (xdiff > mtouchslop && xdiff > ydiff) { if (debug) log.v(tag, "starting drag!"); misbeingdragged = true; mlastmotionx = x; setscrollstate(scroll_state_dragging); setscrollingcacheenabled(true); } } if (misbeingdragged) { // scroll to follow the motion event final int activepointerindex = motioneventcompat.findpointerindex( ev, mactivepointerid); final float x = motioneventcompat.getx(ev, activepointerindex); final float deltax = mlastmotionx - x; mlastmotionx = x; float oldscrollx = getscrollx(); float scrollx = oldscrollx + deltax; final int width = getwidth(); final int widthwithmargin = width + mpagemargin; final int lastitemindex = madapter.getcount() - 1; final float leftbound = math.max(0, (mcuritem - 1) * widthwithmargin); final float rightbound = math.min(mcuritem + 1, lastitemindex) * widthwithmargin; if (scrollx < leftbound) { if (leftbound == 0) { float over = -scrollx; needsinvalidate = mleftedge.onpull(over / width); } scrollx = leftbound; } else if (scrollx > rightbound) { if (rightbound == lastitemindex * widthwithmargin) { float over = scrollx - rightbound; needsinvalidate = mrightedge.onpull(over / width); } scrollx = rightbound; } // don't lose the rounded component mlastmotionx += scrollx - (int) scrollx; scrollto((int) scrollx, getscrolly()); if (monpagechangelistener != null) { final int position = (int) scrollx / widthwithmargin; final int positionoffsetpixels = (int) scrollx % widthwithmargin; final float positionoffset = (float) positionoffsetpixels / widthwithmargin; monpagechangelistener.onpagescrolled(position, positionoffset, positionoffsetpixels); } } break; case motionevent.action_up: if (misbeingdragged) { final velocitytracker velocitytracker = mvelocitytracker; velocitytracker.computecurrentvelocity(1000, mmaximumvelocity); int initialvelocity = (int) velocitytrackercompat.getxvelocity( velocitytracker, mactivepointerid); mpopulatepending = true; final int widthwithmargin = getwidth() + mpagemargin; final int scrollx = getscrollx(); final int currentpage = scrollx / widthwithmargin; int nextpage = initialvelocity > 0 ? currentpage : currentpage + 1; setcurrentiteminternal(nextpage, true, true, initialvelocity); mactivepointerid = invalid_pointer; enddrag(); needsinvalidate = mleftedge.onrelease() | mrightedge.onrelease(); } break; case motionevent.action_cancel: if (misbeingdragged) { setcurrentiteminternal(mcuritem, true, true); mactivepointerid = invalid_pointer; enddrag(); needsinvalidate = mleftedge.onrelease() | mrightedge.onrelease(); } break; case motioneventcompat.action_pointer_down: { final int index = motioneventcompat.getactionindex(ev); final float x = motioneventcompat.getx(ev, index); mlastmotionx = x; mactivepointerid = motioneventcompat.getpointerid(ev, index); break; } case motioneventcompat.action_pointer_up: onsecondarypointerup(ev); mlastmotionx = motioneventcompat.getx(ev, motioneventcompat.findpointerindex(ev, mactivepointerid)); break; } if (needsinvalidate) { invalidate(); } return true; } @override public void draw(canvas canvas) { super.draw(canvas); boolean needsinvalidate = false; final int overscrollmode = viewcompat.getoverscrollmode(this); if (overscrollmode == viewcompat.over_scroll_always || (overscrollmode == viewcompat.over_scroll_if_content_scrolls && madapter != null && madapter.getcount() > 1)) { if (!mleftedge.isfinished()) { final int restorecount = canvas.save(); final int height = getheight() - getpaddingtop() - getpaddingbottom(); canvas.rotate(270); canvas.translate(-height + getpaddingtop(), 0); mleftedge.setsize(height, getwidth()); needsinvalidate |= mleftedge.draw(canvas); canvas.restoretocount(restorecount); } if (!mrightedge.isfinished()) { final int restorecount = canvas.save(); final int width = getwidth(); final int height = getheight() - getpaddingtop() - getpaddingbottom(); final int itemcount = madapter != null ? madapter.getcount() : 1; canvas.rotate(90); canvas.translate(-getpaddingtop(), -itemcount * (width + mpagemargin) + mpagemargin); mrightedge.setsize(height, width); needsinvalidate |= mrightedge.draw(canvas); canvas.restoretocount(restorecount); } } else { mleftedge.finish(); mrightedge.finish(); } if (needsinvalidate) { // keep animating invalidate(); } } @override protected void ondraw(canvas canvas) { super.ondraw(canvas); // draw the margin drawable if needed. if (mpagemargin > 0 && mmargindrawable != null) { final int scrollx = getscrollx(); final int width = getwidth(); final int offset = scrollx % (width + mpagemargin); if (offset != 0) { // pages fit completely when settled; we only need to draw when in between final int left = scrollx - offset + width; mmargindrawable.setbounds(left, 0, left + mpagemargin, getheight()); mmargindrawable.draw(canvas); } } } /** * start a fake drag of the pager. * * <p>a fake drag can be useful if you want to synchronize the motion of the viewpager * with the touch scrolling of another view, while still letting the viewpager * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) * call {@link #fakedragby(float)} to simulate the actual drag motion. call * {@link #endfakedrag()} to complete the fake drag and fling as necessary. * * <p>during a fake drag the viewpager will ignore all touch events. if a real drag * is already in progress, this method will return false. * * @return true if the fake drag began successfully, false if it could not be started. * * @see #fakedragby(float) * @see #endfakedrag() */ public boolean beginfakedrag() { if (misbeingdragged) { return false; } mfakedragging = true; setscrollstate(scroll_state_dragging); minitialmotionx = mlastmotionx = 0; if (mvelocitytracker == null) { mvelocitytracker = velocitytracker.obtain(); } else { mvelocitytracker.clear(); } final long time = systemclock.uptimemillis(); final motionevent ev = motionevent.obtain(time, time, motionevent.action_down, 0, 0, 0); mvelocitytracker.addmovement(ev); ev.recycle(); mfakedragbegintime = time; return true; } /** * end a fake drag of the pager. * * @see #beginfakedrag() * @see #fakedragby(float) */ public void endfakedrag() { if (!mfakedragging) { throw new illegalstateexception("no fake drag in progress. call beginfakedrag first."); } final velocitytracker velocitytracker = mvelocitytracker; velocitytracker.computecurrentvelocity(1000, mmaximumvelocity); int initialvelocity = (int)velocitytrackercompat.getyvelocity( velocitytracker, mactivepointerid); mpopulatepending = true; if ((math.abs(initialvelocity) > mminimumvelocity) || math.abs(minitialmotionx-mlastmotionx) >= (getwidth()/3)) { if (mlastmotionx > minitialmotionx) { setcurrentiteminternal(mcuritem-1, true, true); } else { setcurrentiteminternal(mcuritem+1, true, true); } } else { setcurrentiteminternal(mcuritem, true, true); } enddrag(); mfakedragging = false; } /** * fake drag by an offset in pixels. you must have called {@link #beginfakedrag()} first. * * @param xoffset offset in pixels to drag by. * @see #beginfakedrag() * @see #endfakedrag() */ public void fakedragby(float xoffset) { if (!mfakedragging) { throw new illegalstateexception("no fake drag in progress. call beginfakedrag first."); } mlastmotionx += xoffset; float scrollx = getscrollx() - xoffset; final int width = getwidth(); final int widthwithmargin = width + mpagemargin; final float leftbound = math.max(0, (mcuritem - 1) * widthwithmargin); final float rightbound = math.min(mcuritem + 1, madapter.getcount() - 1) * widthwithmargin; if (scrollx < leftbound) { scrollx = leftbound; } else if (scrollx > rightbound) { scrollx = rightbound; } // don't lose the rounded component mlastmotionx += scrollx - (int) scrollx; scrollto((int) scrollx, getscrolly()); if (monpagechangelistener != null) { final int position = (int) scrollx / widthwithmargin; final int positionoffsetpixels = (int) scrollx % widthwithmargin; final float positionoffset = (float) positionoffsetpixels / widthwithmargin; monpagechangelistener.onpagescrolled(position, positionoffset, positionoffsetpixels); } // synthesize an event for the velocitytracker. final long time = systemclock.uptimemillis(); final motionevent ev = motionevent.obtain(mfakedragbegintime, time, motionevent.action_move, mlastmotionx, 0, 0); mvelocitytracker.addmovement(ev); ev.recycle(); } /** * returns true if a fake drag is in progress. * * @return true if currently in a fake drag, false otherwise. * * @see #beginfakedrag() * @see #fakedragby(float) * @see #endfakedrag() */ public boolean isfakedragging() { return mfakedragging; } private void onsecondarypointerup(motionevent ev) { final int pointerindex = motioneventcompat.getactionindex(ev); final int pointerid = motioneventcompat.getpointerid(ev, pointerindex); if (pointerid == mactivepointerid) { // this was our active pointer going up. choose a new // active pointer and adjust accordingly. final int newpointerindex = pointerindex == 0 ? 1 : 0; mlastmotionx = motioneventcompat.getx(ev, newpointerindex); mactivepointerid = motioneventcompat.getpointerid(ev, newpointerindex); if (mvelocitytracker != null) { mvelocitytracker.clear(); } } } private void enddrag() { misbeingdragged = false; misunabletodrag = false; if (mvelocitytracker != null) { mvelocitytracker.recycle(); mvelocitytracker = null; } } private void setscrollingcacheenabled(boolean enabled) { if (mscrollingcacheenabled != enabled) { mscrollingcacheenabled = enabled; if (use_cache) { final int size = getchildcount(); for (int i = 0; i < size; ++i) { final view child = getchildat(i); if (child.getvisibility() != gone) { child.setdrawingcacheenabled(enabled); } } } } } /** * tests scrollability within child views of v given a delta of dx. * * @param v view to test for horizontal scrollability * @param checkv whether the view v passed should itself be checked for scrollability (true), * or just its children (false). * @param dx delta scrolled in pixels * @param x x coordinate of the active touch point * @param y y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ protected boolean canscroll(view v, boolean checkv, int dx, int x, int y) { if (v instanceof viewgroup) { final viewgroup group = (viewgroup) v; final int scrollx = v.getscrollx(); final int scrolly = v.getscrolly(); final int count = group.getchildcount(); // count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { // todo: add versioned support here for transformed views. // this will not work for transformed views in honeycomb+ final view child = group.getchildat(i); if (x + scrollx >= child.getleft() && x + scrollx < child.getright() && y + scrolly >= child.gettop() && y + scrolly < child.getbottom() && canscroll(child, true, dx, x + scrollx - child.getleft(), y + scrolly - child.gettop())) { return true; } } } return checkv && viewcompat.canscrollhorizontally(v, -dx); } @override public boolean dispatchkeyevent(keyevent event) { // let the focused view and/or our descendants get the key first return super.dispatchkeyevent(event) || executekeyevent(event); } /** * you can call this function yourself to have the scroll view perform * scrolling from a key event, just as if the event had been dispatched to * it by the view hierarchy. * * @param event the key event to execute. * @return return true if the event was handled, else false. */ public boolean executekeyevent(keyevent event) { boolean handled = false; if (event.getaction() == keyevent.action_down) { switch (event.getkeycode()) { case keyevent.keycode_dpad_left: handled = arrowscroll(focus_left); break; case keyevent.keycode_dpad_right: handled = arrowscroll(focus_right); break; case keyevent.keycode_tab: if (keyeventcompat.hasnomodifiers(event)) { handled = arrowscroll(focus_forward); } else if (keyeventcompat.hasmodifiers(event, keyevent.meta_shift_on)) { handled = arrowscroll(focus_backward); } break; } } return handled; } public boolean arrowscroll(int direction) { view currentfocused = findfocus(); if (currentfocused == this) currentfocused = null; boolean handled = false; view nextfocused = focusfinder.getinstance().findnextfocus(this, currentfocused, direction); if (nextfocused != null && nextfocused != currentfocused) { if (direction == view.focus_left) { // if there is nothing to the left, or this is causing us to // jump to the right, then what we really want to do is page left. if (currentfocused != null && nextfocused.getleft() >= currentfocused.getleft()) { handled = pageleft(); } else { handled = nextfocused.requestfocus(); } } else if (direction == view.focus_right) { // if there is nothing to the right, or this is causing us to // jump to the left, then what we really want to do is page right. if (currentfocused != null && nextfocused.getleft() <= currentfocused.getleft()) { handled = pageright(); } else { handled = nextfocused.requestfocus(); } } } else if (direction == focus_left || direction == focus_backward) { // trying to move left and nothing there; try to page. handled = pageleft(); } else if (direction == focus_right || direction == focus_forward) { // trying to move right and nothing there; try to page. handled = pageright(); } if (handled) { playsoundeffect(soundeffectconstants.getcontantforfocusdirection(direction)); } return handled; } boolean pageleft() { if (mcuritem > 0) { setcurrentitem(mcuritem-1, true); return true; } return false; } boolean pageright() { if (madapter != null && mcuritem < (madapter.getcount()-1)) { setcurrentitem(mcuritem+1, true); return true; } return false; } /** * we only want the current page that is being shown to be focusable. */ @override public void addfocusables(arraylist<view> views, int direction, int focusablemode) { final int focusablecount = views.size(); final int descendantfocusability = getdescendantfocusability(); if (descendantfocusability != focus_block_descendants) { for (int i = 0; i < getchildcount(); i++) { final view child = getchildat(i); if (child.getvisibility() == visible) { iteminfo ii = infoforchild(child); if (ii != null && ii.position == mcuritem) { child.addfocusables(views, direction, focusablemode); } } } } // we add ourselves (if focusable) in all cases except for when we are // focus_after_descendants and there are some descendants focusable. this is // to avoid the focus search finding layouts when a more precise search // among the focusable children would be more interesting. if ( descendantfocusability != focus_after_descendants || // no focusable descendants (focusablecount == views.size())) { // note that we can't call the superclass here, because it will // add all views in. so we need to do the same thing view does. if (!isfocusable()) { return; } if ((focusablemode & focusables_touch_mode) == focusables_touch_mode && isintouchmode() && !isfocusableintouchmode()) { return; } if (views != null) { views.add(this); } } } /** * we only want the current page that is being shown to be touchable. */ @override public void addtouchables(arraylist<view> views) { // note that we don't call super.addtouchables(), which means that // we don't call view.addtouchables(). this is okay because a viewpager // is itself not touchable. for (int i = 0; i < getchildcount(); i++) { final view child = getchildat(i); if (child.getvisibility() == visible) { iteminfo ii = infoforchild(child); if (ii != null && ii.position == mcuritem) { child.addtouchables(views); } } } } /** * we only want the current page that is being shown to be focusable. */ @override protected boolean onrequestfocusindescendants(int direction, rect previouslyfocusedrect) { int index; int increment; int end; int count = getchildcount(); if ((direction & focus_forward) != 0) { index = 0; increment = 1; end = count; } else { index = count - 1; increment = -1; end = -1; } for (int i = index; i != end; i += increment) { view child = getchildat(i); if (child.getvisibility() == visible) { iteminfo ii = infoforchild(child); if (ii != null && ii.position == mcuritem) { if (child.requestfocus(direction, previouslyfocusedrect)) { return true; } } } } return false; } @override public boolean dispatchpopulateaccessibilityevent(accessibilityevent event) { // viewpagers should only report accessibility info for the current page, // otherwise things get very confusing. // todo: should this note something about the paging container? final int childcount = getchildcount(); for (int i = 0; i < childcount; i++) { final view child = getchildat(i); if (child.getvisibility() == visible) { final iteminfo ii = infoforchild(child); if (ii != null && ii.position == mcuritem && child.dispatchpopulateaccessibilityevent(event)) { return true; } } } return false; } private class pagerobserver extends datasetobserver { @override public void onchanged() { datasetchanged(); } @override public void oninvalidated() { datasetchanged(); } } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。