先给出一个可运行的例子,MainActivity.java:
11-22 14:41:18.313 32764-447/zhangphil.app D/调试: refreshData:2147483647 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: onDataRefresh:-1,-1 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: 当前RecyclerView为空! 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 0 ~ 9 11-22 14:41:18.337 32764-449/zhangphil.app D/调试: fillData:0,20 11-22 14:41:20.350 32764-32764/zhangphil.app D/调试: onItemLoaded:0 11-22 14:41:20.351 32764-32764/zhangphil.app D/调试: onItemLoaded:1 11-22 14:41:20.351 32764-32764/zhangphil.app D/调试: onItemLoaded:2 11-22 14:41:20.352 32764-32764/zhangphil.app D/调试: onItemLoaded:3 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:4 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:5 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:6
outRange[1]是LastVisibleItemPosition。当这两个参数赋值后,将直接触发fillData,fillData是AsyncListUtil进行长期耗时后台任务的地方,开发者可以在这里处理自己的后台线程任务。
比如现在手指在屏幕上从下往上翻滚RecyclerView,故意翻到没有数据的地方(position=21 ~position=28)然后加载出来,logcat输出:
nullifithasnotbeenloaded
*yet.
*
*
*Ifthismethodhasbeencalledforaspecificpositionandreturnednull,then
*{@linkViewCallback#onItemLoaded(int)}willbecalledwhenitfinallyloads.Notethatif
*thispositionstaysoutsideofthecacheditemrange(asdefinedby
*{@linkViewCallback#extendRangeInto}method),thenthecallbackwillneverbecalledfor
*thisposition.
*
*@parampositionItemposition.
*
*@returnThedataitematthegivenpositionornullifithasnotbeenloaded
*yet.
*/
publicTgetItem(intposition){
if(position<0||position>=mItemCount){
thrownewIndexOutOfBoundsException(position+“isnotwithin0and”+mItemCount);
}
Titem=mTileList.getItemAt(position);
if(item==null&&!isRefreshPending()){
mMissingPositions.put(position,0);
}
returnitem;
}
/**
*Returnsthenumberofitemsinthedataset.
*
*
*Thisisthenumberreturnedbyarecentcallto
*{@linkDataCallback#refreshData()}.
*
*@returnNumberofitems.
*/
publicintgetItemCount(){
returnmItemCount;
}
voidupdateRange(){
mViewCallback.getItemRangeInto(mTmpRange);
if(mTmpRange[0]>mTmpRange[1]||mTmpRange[0]<0){
return;
}
if(mTmpRange[1]>=mItemCount){
//Invalidrangemayarrivesoonaftertherefresh.
return;
}
if(!mAllowScrollHints){
mScrollHint=ViewCallback.HINT_SCROLL_NONE;
}elseif(mTmpRange[0]>mPrevRange[1]||mPrevRange[0]>mTmpRange[1]){
//Rangesdonotintersect,longleapnotascroll.
mScrollHint=ViewCallback.HINT_SCROLL_NONE;
}elseif(mTmpRange[0]<>mScrollHint=ViewCallback.HINT_SCROLL_DESC;
}elseif(mTmpRange[0]>mPrevRange[0]){
mScrollHint=ViewCallback.HINT_SCROLL_ASC;
}
mPrevRange[0]=mTmpRange[0];
mPrevRange[1]=mTmpRange[1];
mViewCallback.extendRangeInto(mTmpRange,mTmpRangeExtended,mScrollHint);
mTmpRangeExtended[0]=Math.min(mTmpRange[0],Math.max(mTmpRangeExtended[0],0));
mTmpRangeExtended[1]=
Math.max(mTmpRange[1],Math.min(mTmpRangeExtended[1],mItemCount-1));
mBackgroundProxy.updateRange(mTmpRange[0],mTmpRange[1],
mTmpRangeExtended[0],mTmpRangeExtended[1],mScrollHint);
}
privatefinalThreadUtil.MainThreadCallback
mMainThreadCallback=newThreadUtil.MainThreadCallback(){
@Override
publicvoidupdateItemCount(intgeneration,intitemCount){
if(DEBUG){
log(”updateItemCount:size=%d,gen#%d”,itemCount,generation);
}
if(!isRequestedGeneration(generation)){
return;
}
mItemCount=itemCount;
mViewCallback.onDataRefresh();
mDisplayedGeneration=mRequestedGeneration;
recycleAllTiles();
mAllowScrollHints=false;//Willbesettotrueafterafirstrealscroll.
//Therewillbenoscrolleventifthesizechangedoesnotaffectthecurrentrange.
updateRange();
}
@Override
publicvoidaddTile(intgeneration,TileList.Tiletile){
if(!isRequestedGeneration(generation)){
if(DEBUG){
log(”recyclinganoldergenerationtile@%d”,tile.mStartPosition);
}
mBackgroundProxy.recycleTile(tile);
return;
}
TileList.Tileduplicate=mTileList.addOrReplace(tile);
if(duplicate!=null){
Log.e(TAG,”duplicatetile@”+duplicate.mStartPosition);
mBackgroundProxy.recycleTile(duplicate);
}
if(DEBUG){
log(”gen#%d,addedtile@%d,totaltiles:%d”,
generation,tile.mStartPosition,mTileList.size());
}
intendPosition=tile.mStartPosition+tile.mItemCount;
intindex=0;
while(index<>finalintposition=mMissingPositions.keyAt(index);
if(tile.mStartPosition<=position&&position<>mMissingPositions.removeAt(index);
mViewCallback.onItemLoaded(position);
}else{
index++;
}
}
}
@Override
publicvoidremoveTile(intgeneration,intposition){
if(!isRequestedGeneration(generation)){
return;
}
TileList.Tiletile=mTileList.removeAtPos(position);
if(tile==null){
Log.e(TAG,”tilenotfound@”+position);
return;
}
if(DEBUG){
log(”recyclingtile@%d,totaltiles:%d”,tile.mStartPosition,mTileList.size());
}
mBackgroundProxy.recycleTile(tile);
}
privatevoidrecycleAllTiles(){
if(DEBUG){
log(”recyclingall%dtiles”,mTileList.size());
}
for(inti=0;i<>mBackgroundProxy.recycleTile(mTileList.getAtIndex(i));
}
mTileList.clear();
}
privatebooleanisRequestedGeneration(intgeneration){
returngeneration==mRequestedGeneration;
}
};
privatefinalThreadUtil.BackgroundCallback
mBackgroundCallback=newThreadUtil.BackgroundCallback(){
privateTileList.TilemRecycledRoot;
finalSparseBooleanArraymLoadedTiles=newSparseBooleanArray();
privateintmGeneration;
privateintmItemCount;
privateintmFirstRequiredTileStart;
privateintmLastRequiredTileStart;
@Override
publicvoidrefresh(intgeneration){
mGeneration=generation;
mLoadedTiles.clear();
mItemCount=mDataCallback.refreshData();
mMainThreadProxy.updateItemCount(mGeneration,mItemCount);
}
@Override
publicvoidupdateRange(intrangeStart,intrangeEnd,intextRangeStart,intextRangeEnd,
intscrollHint){
if(DEBUG){
log(”updateRange:%d..%dextendedto%d..%d,scrollhint:%d”,
rangeStart,rangeEnd,extRangeStart,extRangeEnd,scrollHint);
}
if(rangeStart>rangeEnd){
return;
}
finalintfirstVisibleTileStart=getTileStart(rangeStart);
finalintlastVisibleTileStart=getTileStart(rangeEnd);
mFirstRequiredTileStart=getTileStart(extRangeStart);
mLastRequiredTileStart=getTileStart(extRangeEnd);
if(DEBUG){
log(”requestingtilerange:%d..%d”,
mFirstRequiredTileStart,mLastRequiredTileStart);
}
//AllpendingtilerequestsareremovedbyThreadUtilatthispoint.
//Re-requestallrequiredtilesinthemostoptimalorder.
if(scrollHint==ViewCallback.HINT_SCROLL_DESC){
requestTiles(mFirstRequiredTileStart,lastVisibleTileStart,scrollHint,true);
requestTiles(lastVisibleTileStart+mTileSize,mLastRequiredTileStart,scrollHint,
false);
}else{
requestTiles(firstVisibleTileStart,mLastRequiredTileStart,scrollHint,false);
requestTiles(mFirstRequiredTileStart,firstVisibleTileStart-mTileSize,scrollHint,
true);
}
}
privateintgetTileStart(intposition){
returnposition-position%mTileSize;
}
privatevoidrequestTiles(intfirstTileStart,intlastTileStart,intscrollHint,
booleanbackwards){
for(inti=firstTileStart;i<=lastTileStart;i+=mTileSize){
inttileStart=backwards?(lastTileStart+firstTileStart-i):i;
if(DEBUG){
log(”requestingtile@%d”,tileStart);
}
mBackgroundProxy.loadTile(tileStart,scrollHint);
}
}
@Override
publicvoidloadTile(intposition,intscrollHint){
if(isTileLoaded(position)){
if(DEBUG){
log(”alreadyloadedtile@%d”,position);
}
return;
}
TileList.Tiletile=acquireTile();
tile.mStartPosition=position;
tile.mItemCount=Math.min(mTileSize,mItemCount-tile.mStartPosition);
mDataCallback.fillData(tile.mItems,tile.mStartPosition,tile.mItemCount);
flushTileCache(scrollHint);
addTile(tile);
}
@Override
publicvoidrecycleTile(TileList.Tiletile){
if(DEBUG){
log(”recyclingtile@%d”,tile.mStartPosition);
}
mDataCallback.recycleData(tile.mItems,tile.mItemCount);
tile.mNext=mRecycledRoot;
mRecycledRoot=tile;
}
privateTileList.TileacquireTile(){
if(mRecycledRoot!=null){
TileList.Tileresult=mRecycledRoot;
mRecycledRoot=mRecycledRoot.mNext;
returnresult;
}
returnnewTileList.Tile(mTClass,mTileSize);
}
privatebooleanisTileLoaded(intposition){
returnmLoadedTiles.get(position);
}
privatevoidaddTile(TileList.Tiletile){
mLoadedTiles.put(tile.mStartPosition,true);
mMainThreadProxy.addTile(mGeneration,tile);
if(DEBUG){
log(”loadedtile@%d,totaltiles:%d”,tile.mStartPosition,mLoadedTiles.size());
}
}
privatevoidremoveTile(intposition){
mLoadedTiles.delete(position);
mMainThreadProxy.removeTile(mGeneration,position);
if(DEBUG){
log(”flushedtile@%d,totaltiles:%s”,position,mLoadedTiles.size());
}
}
privatevoidflushTileCache(intscrollHint){
finalintcacheSizeLimit=mDataCallback.getMaxCachedTiles();
while(mLoadedTiles.size()>=cacheSizeLimit){
intfirstLoadedTileStart=mLoadedTiles.keyAt(0);
intlastLoadedTileStart=mLoadedTiles.keyAt(mLoadedTiles.size()-1);
intstartMargin=mFirstRequiredTileStart-firstLoadedTileStart;
intendMargin=lastLoadedTileStart-mLastRequiredTileStart;
if(startMargin>0&&(startMargin>=endMargin||
(scrollHint==ViewCallback.HINT_SCROLL_ASC))){
removeTile(firstLoadedTileStart);
}elseif(endMargin>0&&(startMargin<>(scrollHint==ViewCallback.HINT_SCROLL_DESC))){
removeTile(lastLoadedTileStart);
}else{
//Couldnotflushoneitherside,bailout.
return;
}
}
}
privatevoidlog(Strings,Object…args){
Log.d(TAG,”[BKGR]”+String.format(s,args));
}
};
/**
*Thecallbackthatprovidesdataaccessfor{@linkAsyncListUtil}.
*
*
*Allmethodsarecalledonthebackgroundthread.
*/
publicstaticabstractclassDataCallback{
/**
*Refreshthedatasetandreturnthenewdataitemcount.
*
*
*Ifthedataisbeingaccessedthrough{@linkandroid.database.Cursor}thisiswhere
*thenewcursorshouldbecreated.
*
*@returnDataitemcount.
*/
@WorkerThread
publicabstractintrefreshData();
/**
*Fillthegiventile.
*
*
*Theprovidedtilemightbearecycledtile,inwhichcaseitwillalreadyhaveobjects.
*Itissuggestedtore-usetheseobjectsifpossibleinyourusecase.
*
*@paramstartPositionThestartpositioninthelist.
*@paramitemCountThedataitemcount.
*@paramdataThedataitemarraytofillinto.Shouldnotbeaccessedbeyond
*itemCount.
*/
@WorkerThread
publicabstractvoidfillData(T[]data,intstartPosition,intitemCount);
/**
*Recycletheobjectscreatedin{@link#fillData}ifnecessary.
*
*
*@paramdataArrayofdataitems.ShouldnotbeaccessedbeyonditemCount.
*@paramitemCountThedataitemcount.
*/
@WorkerThread
publicvoidrecycleData(T[]data,intitemCount){
}
/**
*Returnstilecachesizelimit(intiles).
*
*
*Theactualnumberofcachedtileswillbethemaximumofthisvalueandthenumberof
*tilesthatisrequiredtocovertherangereturnedby
*{@linkViewCallback#extendRangeInto(int[],int[],int)}.
*
*Forexample,ifthismethodreturns10,andthemost
*recentcallto{@linkViewCallback#extendRangeInto(int[],int[],int)}returned
*{100,179},andthetilesizeis5,thenthemaximumnumberofcachedtileswillbe16.
*
*However,ifthetilesizeis20,thenthemaximumnumberofcachedtileswillbe10.
*
*Thedefaultimplementationreturns10.
*
*@returnMaximumcachesize.
*/
@WorkerThread
publicintgetMaxCachedTiles(){
return10;
}
}
/**
*Thecallbackthatlinks{@linkAsyncListUtil}withthelistview.
*
*
*Allmethodsarecalledonthemainthread.
*/
publicstaticabstractclassViewCallback{
/**
*Noscrolldirectionhintavailable.
*/
publicstaticfinalintHINT_SCROLL_NONE=0;
/**
*Scrollingindescendingorder(fromhighertolowerpositionsintheorderofthebacking
*storage).
*/
publicstaticfinalintHINT_SCROLL_DESC=1;
/**
*Scrollinginascendingorder(fromlowertohigherpositionsintheorderofthebacking
*storage).
*/
publicstaticfinalintHINT_SCROLL_ASC=2;
/**
*Computetherangeofvisibleitempositions.
*
*outRange[0]isthepositionofthefirstvisibleitem(intheorderofthebacking
*storage).
*
*outRange[1]isthepositionofthelastvisibleitem(intheorderofthebacking
*storage).
*
*Negativepositionsandpositionsgreaterorequalto{@link#getItemCount}areinvalid.
*Ifthereturnedrangecontainsinvalidpositionsitisignored(noitemwillbeloaded).
*
*@paramoutRangeThevisibleitemrange.
*/
@UiThread
publicabstractvoidgetItemRangeInto(int[]outRange);
/**
*Computeawiderrangeofitemsthatwillbeloadedforsmootherscrolling.
*
*
*Ifthereisnoscrollhint,thedefaultimplementationextendsthevisiblerangebyhalf
*itslengthinbothdirections.Ifthereisascrollhint,therangeisextendedby
*itsfulllengthinthescrolldirection,andbyhalfintheotherdirection.
*
*Forexample,ifrangeis{100,200}andscrollHint
*is{@link#HINT_SCROLL_ASC},thenoutRangewillbe{50,300}.
*
*However,ifscrollHintis{@link#HINT_SCROLL_NONE},then
*outRangewillbe{50,250}
*
*@paramrangeVisibleitemrange.
*@paramoutRangeExtendedrange.
*@paramscrollHintThescrolldirectionhint.
*/
@UiThread
publicvoidextendRangeInto(int[]range,int[]outRange,intscrollHint){
finalintfullRange=range[1]-range[0]+1;
finalinthalfRange=fullRange/2;
outRange[0]=range[0]-(scrollHint==HINT_SCROLL_DESC?fullRange:halfRange);
outRange[1]=range[1]+(scrollHint==HINT_SCROLL_ASC?fullRange:halfRange);
}
/**
*Calledwhentheentiredatasethaschanged.
*/
@UiThread
publicabstractvoidonDataRefresh();
/**
*Calledwhenanitematthegivenpositionisloaded.
*@parampositionItemposition.
*/
@UiThread
publicabstractvoidonItemLoaded(intposition);
}
}
/*
* Copyright (C) 2015 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.
*/
package android.support.v7.util;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
/**
* A utility class that supports asynchronous content loading.
*
* It can be used to load Cursor data in chunks without querying the Cursor on the UI Thread while * keeping UI and cache synchronous for better user experience. *
* It loads the data on a background thread and keeps only a limited number of fixed sized * chunks in memory at all times. *
* {@link AsyncListUtil} queries the currently visible range through {@link ViewCallback}, * loads the required data items in the background through {@link DataCallback}, and notifies a * {@link ViewCallback} when the data is loaded. It may load some extra items for smoother * scrolling. *
* Note that this class uses a single thread to load the data, so it suitable to load data from * secondary storage such as disk, but not from network. *
* This class is designed to work with {@link android.support.v7.widget.RecyclerView}, but it does * not depend on it and can be used with other list views. * */ public class AsyncListUtil
* Identifies the data items that have not been loaded yet and initiates loading them in the * background. Should be called from the view's scroll listener (such as * {@link android.support.v7.widget.RecyclerView.OnScrollListener#onScrolled}). */ public void onRangeChanged() { if (isRefreshPending()) { return; // Will update range will the refresh result arrives. } updateRange(); mAllowScrollHints = true; } /** * Forces reloading the data. *
* Discards all the cached data and reloads all required data items for the currently visible * range. To be called when the data item count and/or contents has changed. */ public void refresh() { mMissingPositions.clear(); mBackgroundProxy.refresh(++mRequestedGeneration); } /** * Returns the data item at the given position or
null if it has not been loaded * yet. * *
* If this method has been called for a specific position and returned
null, then * {@link ViewCallback#onItemLoaded(int)} will be called when it finally loads. Note that if * this position stays outside of the cached item range (as defined by * {@link ViewCallback#extendRangeInto} method), then the callback will never be called for * this position. * * @param position Item position. * * @return The data item at the given position or
null if it has not been loaded * yet. */ public T getItem(int position) { if (position < 0 || position >= mItemCount) { throw new IndexOutOfBoundsException(position + " is not within 0 and " + mItemCount); } T item = mTileList.getItemAt(position); if (item == null && !isRefreshPending()) { mMissingPositions.put(position, 0); } return item; } /** * Returns the number of items in the data set. * *
* This is the number returned by a recent call to * {@link DataCallback#refreshData()}. * * @return Number of items. */ public int getItemCount() { return mItemCount; } void updateRange() { mViewCallback.getItemRangeInto(mTmpRange); if (mTmpRange[0] > mTmpRange[1] || mTmpRange[0] < 0) { return; } if (mTmpRange[1] >= mItemCount) { // Invalid range may arrive soon after the refresh. return; } if (!mAllowScrollHints) { mScrollHint = ViewCallback.HINT_SCROLL_NONE; } else if (mTmpRange[0] > mPrevRange[1] || mPrevRange[0] > mTmpRange[1]) { // Ranges do not intersect, long leap not a scroll. mScrollHint = ViewCallback.HINT_SCROLL_NONE; } else if (mTmpRange[0] < mPrevRange[0]) { mScrollHint = ViewCallback.HINT_SCROLL_DESC; } else if (mTmpRange[0] > mPrevRange[0]) { mScrollHint = ViewCallback.HINT_SCROLL_ASC; } mPrevRange[0] = mTmpRange[0]; mPrevRange[1] = mTmpRange[1]; mViewCallback.extendRangeInto(mTmpRange, mTmpRangeExtended, mScrollHint); mTmpRangeExtended[0] = Math.min(mTmpRange[0], Math.max(mTmpRangeExtended[0], 0)); mTmpRangeExtended[1] = Math.max(mTmpRange[1], Math.min(mTmpRangeExtended[1], mItemCount - 1)); mBackgroundProxy.updateRange(mTmpRange[0], mTmpRange[1], mTmpRangeExtended[0], mTmpRangeExtended[1], mScrollHint); } private final ThreadUtil.MainThreadCallback
* All methods are called on the background thread. */ public static abstract class DataCallback
* If the data is being accessed through {@link android.database.Cursor} this is where * the new cursor should be created. * * @return Data item count. */ @WorkerThread public abstract int refreshData(); /** * Fill the given tile. * *
* The provided tile might be a recycled tile, in which case it will already have objects. * It is suggested to re-use these objects if possible in your use case. * * @param startPosition The start position in the list. * @param itemCount The data item count. * @param data The data item array to fill into. Should not be accessed beyond *
itemCount. */ @WorkerThread public abstract void fillData(T[] data, int startPosition, int itemCount); /** * Recycle the objects created in {@link #fillData} if necessary. * * * @param data Array of data items. Should not be accessed beyond
itemCount. * @param itemCount The data item count. */ @WorkerThread public void recycleData(T[] data, int itemCount) { } /** * Returns tile cache size limit (in tiles). * *
* The actual number of cached tiles will be the maximum of this value and the number of * tiles that is required to cover the range returned by * {@link ViewCallback#extendRangeInto(int[], int[], int)}. *
* For example, if this method returns 10, and the most * recent call to {@link ViewCallback#extendRangeInto(int[], int[], int)} returned * {100, 179}, and the tile size is 5, then the maximum number of cached tiles will be 16. *
* However, if the tile size is 20, then the maximum number of cached tiles will be 10. *
* The default implementation returns 10. * * @return Maximum cache size. */ @WorkerThread public int getMaxCachedTiles() { return 10; } } /** * The callback that links {@link AsyncListUtil} with the list view. * *
* All methods are called on the main thread. */ public static abstract class ViewCallback { /** * No scroll direction hint available. */ public static final int HINT_SCROLL_NONE = 0; /** * Scrolling in descending order (from higher to lower positions in the order of the backing * storage). */ public static final int HINT_SCROLL_DESC = 1; /** * Scrolling in ascending order (from lower to higher positions in the order of the backing * storage). */ public static final int HINT_SCROLL_ASC = 2; /** * Compute the range of visible item positions. *
* outRange[0] is the position of the first visible item (in the order of the backing * storage). *
* outRange[1] is the position of the last visible item (in the order of the backing * storage). *
* Negative positions and positions greater or equal to {@link #getItemCount} are invalid. * If the returned range contains invalid positions it is ignored (no item will be loaded). * * @param outRange The visible item range. */ @UiThread public abstract void getItemRangeInto(int[] outRange); /** * Compute a wider range of items that will be loaded for smoother scrolling. * *
* If there is no scroll hint, the default implementation extends the visible range by half * its length in both directions. If there is a scroll hint, the range is extended by * its full length in the scroll direction, and by half in the other direction. *
* For example, if
range is
{100, 200} and
scrollHint * is {@link #HINT_SCROLL_ASC}, then
outRange will be
{50, 300}. *
* However, if
scrollHint is {@link #HINT_SCROLL_NONE}, then *
outRange will be
{50, 250} * * @param range Visible item range. * @param outRange Extended range. * @param scrollHint The scroll direction hint. */ @UiThread public void extendRangeInto(int[] range, int[] outRange, int scrollHint) { final int fullRange = range[1] - range[0] + 1; final int halfRange = fullRange / 2; outRange[0] = range[0] - (scrollHint == HINT_SCROLL_DESC ? fullRange : halfRange); outRange[1] = range[1] + (scrollHint == HINT_SCROLL_ASC ? fullRange : halfRange); } /** * Called when the entire data set has changed. */ @UiThread public abstract void onDataRefresh(); /** * Called when an item at the given position is loaded. * @param position Item position. */ @UiThread public abstract void onItemLoaded(int position); } }