我有一个我扩展的自定义 AdapterView,它承载了 RelativeLayouts,它又包含显示数据的 Gallery 视图。需要为父适配器视图中的每个项目替换该数据。
在父AdapterView的适配器类的实现中,
我需要在getView()方法中更改嵌套Gallery的适配器,当我得到一个回收的convertView参数时(父适配器视图中的每一项对应不同的数据集显示在嵌套的画廊视图中)。
此代码尝试在内部更改适配器本身的数据集。假设之前的数据集有 3 个项目,而新的数据集有 1 个,它将更新数据集中的第一个视图,但会将额外的视图(它应该删除)留在原处。无法滚动到多余的不必要的视图(画廊的行为就好像它只有一个视图,而实际上它没有)。
截屏:
代码:
获取视图:
@Override
protected View getView(int position, View convertView) {
CollectionGalleryAdapter sla;
IMediaItem item = mCollection.get(position);
ViewHolder holder = null;
if (convertView == null) {
convertView = View.inflate(mContext, layout.stream_item, null);
holder = new ViewHolder();
convertView.setTag(holder);
holder.mGallery = (Gallery) convertView
.findViewById(android.R.id.list);
holder.mTime = (TextView) convertView.findViewById(id.txt_time);
holder.mTitle = (TextView) convertView.findViewById(id.title);
holder.mBg = (ImageView) convertView.findViewById(id.thumb);
sla = new CollectionGalleryAdapter(mContext,
(MediaCollection) item.getExtra(), App.instance()
.getImageLoader(), holder.mGallery);
final View fv = convertView;
sla.setBitmapLoadedListener(new OnBitmapLoadedListener() {
@Override
public void onBitmapLoaded(View v, int position, Bitmap b) {
mStreamView.invalidateChild(fv);
}
});
holder.mGallery.setAdapter(sla);
} else {
holder = (ViewHolder) convertView.getTag();
sla = (CollectionGalleryAdapter) holder.mGallery.getAdapter();
sla.setCollection((MediaCollection) item.getExtra());
}
}
CollectionGalleryAdapter.setCollection(注意对 notifyDataSetChanged 的调用)
public void setCollection(MediaCollection collection) {
mCollection = collection;
mAdapterView.setSelection(0);
notifyDataSetChanged(); // *****
clearLoaderQueue();
postLoad(); // load bitmaps for gallery images
}
自定义适配器视图:
public class StreamView extends AdapterView<Adapter> implements
OnScrollListener {
/** Represents an invalid child index */
private static final int INVALID_INDEX = -1;
/** Distance to drag before we intercept touch events */
private static final int TOUCH_SCROLL_THRESHOLD = 10;
/** Children added with this layout mode will be added below the last child */
private static final int LAYOUT_MODE_BELOW = 0;
/** Children added with this layout mode will be added above the first child */
private static final int LAYOUT_MODE_ABOVE = 1;
/** User is not touching the list */
private static final int TOUCH_STATE_RESTING = 0;
/** User is touching the list and right now it's still a "click" */
private static final int TOUCH_STATE_CLICK = 1;
/** User is scrolling the list */
private static final int TOUCH_STATE_SCROLL = 2;
private static final int BASE_ALPHA = 0x22;
private static final float HEIGHT_FACTOR = 1.7f;
/** The adapter with all the data */
private Adapter mAdapter;
/** Current touch state */
private int mTouchState = TOUCH_STATE_RESTING;
/** X-coordinate of the down event */
private int mTouchStartX;
/** Y-coordinate of the down event */
private int mTouchStartY;
/**
* The top of the first item when the touch down event was received
*/
private int mListTopStart;
/** The current top of the first item */
private int mListTop;
/**
* The offset from the top of the currently first visible item to the top of
* the first item
*/
private int mListTopOffset;
/** The adaptor position of the first visible item */
private int mFirstItemPosition;
/** The adaptor position of the last visible item */
private int mLastItemPosition;
/** A list of cached (re-usable) item views */
private final LinkedList<View> mCachedItemViews = new LinkedList<View>();
/** Used to check for long press actions */
private Runnable mLongPressRunnable;
/** Reusable rect */
private Rect mRect = new Rect();
private Canvas mCanvas = new Canvas();
private Paint mPaint;
private ElasticScroller mScroller;
private int mChildHeight;
private int mLayoutHeight;
private DisplayMetrics mDisplayMetrics;
// private WeakHashMap<View, Bitmap> mDrawingCache;
// private WeakHashMap<View, Boolean> mInvalidateMap;
/**
* Constructor
*
* @param context
* The context
* @param attrs
* Attributes
*/
public StreamView(final Context context, final AttributeSet attrs) {
super(context, attrs);
mScroller = new ElasticScroller(new Handler(), this,
ElasticScroller.AXIS_VERTICAL, 0, getHeight() / 3);
}
View mMotionTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final Rect frame = mRect;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (!onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) xf;
final int scrolledYInt = (int) yf;
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
final int actualtop = getActualChildTop(child);
frame.set(0, 0, getWidth(), mChildHeight);
frame.offset(0, actualtop);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
ev.setLocation(xf, yf - actualtop);
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
child.setTag(string.tag_invalidate, true);
// mInvalidateMap.put(child, true);
invalidate();
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP)
|| (action == MotionEvent.ACTION_CANCEL);
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
return onTouchEvent(ev);
}
// if have a target, see if we're allowed to and want to intercept its
// events
if (onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xf, yf - getActualChildTop(mMotionTarget));
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
} else {
target.setTag(string.tag_invalidate, true);
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
ev.setLocation(xf, yf - getActualChildTop(target));
if (target.dispatchTouchEvent(ev)) {
target.setTag(string.tag_invalidate, true);
invalidate();
return true;
} else {
return false;
}
}
MotionEvent mDownEvent;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int index = getContainingChildIndex((int) ev.getX(), (int) ev.getY());
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchStartX = (int) ev.getX();
mTouchStartY = (int) ev.getY();
mTouchState = TOUCH_STATE_CLICK;
mDownEvent = MotionEvent.obtain(ev);
return false;
case MotionEvent.ACTION_MOVE:
int ydistance = (int) Math.abs(ev.getY() - mTouchStartY);
int xdistance = (int) Math.abs(ev.getX() - mTouchStartX);
if ((index == getChildCount() - 1 && ydistance <= xdistance)
|| ydistance <= ViewConfiguration.getTouchSlop()) {
return false;
} else {
mScroller.onTouch(this, mDownEvent);
mTouchState = TOUCH_STATE_SCROLL;
return true;
}
case MotionEvent.ACTION_UP:
if (index != getChildCount() - 1
&& mTouchState == TOUCH_STATE_CLICK) {
mScroller
.scrollByDistance(
(Integer) getChildAt(index).getTag(
string.tag_top), 400);
return false;
}
// mTouchState = TOUCH_STATE_RESTING;
return false;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public void setAdapter(final Adapter adapter) {
mAdapter = adapter;
removeAllViewsInLayout();
requestLayout();
}
@Override
public Adapter getAdapter() {
return mAdapter;
}
@Override
public void setSelection(final int position) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public View getSelectedView() {
throw new UnsupportedOperationException("Not supported");
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mScroller.onTouch(this, event);
return true;
}
/*
* @Override public boolean onInterceptTouchEvent(final MotionEvent event) {
* switch (event.getAction()) { case MotionEvent.ACTION_DOWN:
* startTouch(event); return false;
*
* case MotionEvent.ACTION_MOVE: return startScrollIfNeeded(event);
*
* default: endTouch(); return false; } return true; }
*
* @Override public boolean onTouchEvent(final MotionEvent event) { /* if
* (getChildCount() == 0) { return false; } switch (event.getAction()) {
* case MotionEvent.ACTION_DOWN: startTouch(event); break;
*
* case MotionEvent.ACTION_MOVE: if (mTouchState == TOUCH_STATE_CLICK) {
* startScrollIfNeeded(event); } if (mTouchState == TOUCH_STATE_SCROLL) {
* scrollList((int) event.getY() - mTouchStartY); } break;
*
* case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_CLICK) {
* clickChildAt((int) event.getX(), (int) event.getY()); } endTouch();
* break;
*
* default: endTouch(); break; } return true; }
*/
@Override
protected void onLayout(final boolean changed, final int left,
final int top, final int right, final int bottom) {
super.onLayout(changed, left, top, right, bottom);
// if we don't have an adapter, we don't need to do anything
if (mAdapter == null) {
return;
}
mLayoutHeight = bottom - top;
if (getChildCount() == 0) {
mLastItemPosition = -1;
fillListDown(mListTop, 0);
} else {
final int offset = mListTop
+ mListTopOffset
- ((Integer) getChildAt(getChildCount() - 1).getTag(
string.tag_top));
removeNonVisibleViews(offset);
fillList(offset);
}
positionItems(changed);
invalidate();
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (mTouchState != TOUCH_STATE_SCROLL) {
if ((Integer) child.getTag(string.tag_top) == 0) {
invalidate();
child.draw(canvas);
return true;
}
}
Bitmap b = (Bitmap) child.getTag(string.tag_drawing_cache);// child.getDrawingCache();
Boolean invalidated = (Boolean) child.getTag(string.tag_invalidate);
if (b == null || (invalidated != null && invalidated == true)) {
if (b == null) {
do {
try {
b = Bitmap.createBitmap(getWidth(), mChildHeight,
Config.ARGB_8888);
break;
} catch (OutOfMemoryError e) {
System.gc();
}
} while (App.instance(getContext()).freeCacheMemory());
if (b == null) {
return false;
}
} else {
b.eraseColor(Color.TRANSPARENT);
}
mCanvas.setBitmap(b);
child.draw(mCanvas);
child.setTag(string.tag_drawing_cache, b);
child.setTag(string.tag_invalidate, false);
}
if (mPaint == null) {
mPaint = new Paint();
}
if (mDisplayMetrics == null) {
mDisplayMetrics = getResources().getDisplayMetrics();
}
final int top = (Integer) child.getTag(string.tag_top);
final int cheight = mChildHeight;
final int height = getHeight();
final int bottom = height - cheight;
final float x = Math.min(1f, (float) top / (height * HEIGHT_FACTOR));
final float y = (float) top / cheight;
final int newtop;
final float t = (float) (1 - (Math
.pow((1 - (Math.min(1f, x * 1.2))), 2)));
if (x >= 0) {
if (x <= 1f) {
newtop = (int) (bottom * t);
} else {
newtop = bottom;
}
} else {
newtop = (int) ((cheight - mDisplayMetrics.density * 65) * y);
}
final int alpha = BASE_ALPHA
+ (int) ((255 - BASE_ALPHA) * (x < 0 ? 1f : (1f - t)));
mPaint.setColorFilter(new LightingColorFilter(alpha | (alpha << 8)
| (alpha << 16), 0));
mPaint.setAlpha((int) (255 * (x >= 0.8 ? (float) Math.pow(
1 - (x - 0.8) / 0.2, 1 / 2f) : (x >= 0 ? 1f : 1f - Math.pow(y,
4)))));
canvas.drawBitmap(b, 0, newtop, mPaint);
return false;
}
private int getActualChildTop(View child) {
final int top = (Integer) child.getTag(string.tag_top);
final int cheight = mChildHeight;
final int height = getHeight();
final int bottom = height - cheight;
final float x = Math.min(1f, (float) top / (height * HEIGHT_FACTOR));
final float y = (float) top / cheight;
final float t = (float) (1 - (Math
.pow((1 - (Math.min(1f, x * 1.2))), 2)));
if (x >= 0) {
if (x <= 1f) {
return (int) (bottom * t);
} else {
return bottom;
}
} else {
return (int) ((cheight - mDisplayMetrics.density * 65) * y);
}
}
/**
* Returns the index of the child that contains the coordinates given.
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return The index of the child that contains the coordinates. If no child
* is found then it returns INVALID_INDEX
*/
private int getContainingChildIndex(final int x, final int y) {
if (mRect == null) {
mRect = new Rect();
}
for (int index = getChildCount() - 1; index >= 0; index--) {
View child = getChildAt(index);
mRect.set(0, 0, getWidth(), mChildHeight);
mRect.offset(0, getActualChildTop(child));
if (mRect.contains(x, y)) {
return index;
}
}
return INVALID_INDEX;
}
/**
* Removes view that are outside of the visible part of the list. Will not
* remove all views.
*
* @param offset
* Offset of the visible area
*/
private void removeNonVisibleViews(final int offset) {
// We need to keep close track of the child count in this function. We
// should never remove all the views, because if we do, we loose track
// of were we are.
int childCount = getChildCount();
// if we are not at the bottom of the list and have more than one child
if (mLastItemPosition != mAdapter.getCount() - 1 && childCount > 1) {
// check if we should remove any views in the top
View firstChild = getChildAt(childCount - 1);
while (firstChild != null
&& ((Integer) firstChild.getTag(string.tag_top))
+ mChildHeight + offset <= 0) {
// remove the top view
removeViewInLayout(firstChild);
childCount--;
mCachedItemViews.addLast(firstChild);
firstChild.setTag(string.tag_invalidate, true);
mFirstItemPosition++;
// update the list offset (since we've removed the top child)
mListTopOffset += firstChild.getMeasuredHeight();
// Continue to check the next child only if we have more than
// one child left
if (childCount > 1) {
firstChild = getChildAt(childCount - 1);
} else {
firstChild = null;
}
}
}
// if we are not at the top of the list and have more than one child
if (mFirstItemPosition != 0 && childCount > 1) {
// check if we should remove any views in the bottom
View lastChild = getChildAt(0);
while (lastChild != null
&& ((Integer) lastChild.getTag(string.tag_top)) + offset > getHeight()
* HEIGHT_FACTOR) {
// remove the bottom view
removeViewInLayout(lastChild);
childCount--;
mCachedItemViews.addLast(lastChild);
lastChild.setTag(string.tag_invalidate, true);
mLastItemPosition--;
// Continue to check the next child only if we have more than
// one child left
if (childCount > 1) {
lastChild = getChildAt(0);
} else {
lastChild = null;
}
}
}
}
/**
* Fills the list with child-views
*
* @param offset
* Offset of the visible area
*/
private void fillList(final int offset) {
if (mChildHeight == 0) {
mChildHeight = getChildAt(0).getMeasuredHeight();
}
final int bottomEdge = ((Integer) getChildAt(0).getTag(string.tag_top))
+ mChildHeight;
fillListDown(bottomEdge, offset);
final int topEdge = (Integer) getChildAt(getChildCount() - 1).getTag(
string.tag_top);
fillListUp(topEdge, offset);
}
/**
* Starts at the bottom and adds children until we've passed the list bottom
*
* @param bottomEdge
* The bottom edge of the currently last child
* @param offset
* Offset of the visible area
*/
private void fillListDown(int bottomEdge, final int offset) {
while (bottomEdge + offset < getHeight() * HEIGHT_FACTOR
&& mLastItemPosition < mAdapter.getCount() - 1) {
mLastItemPosition++;
final View newBottomchild = mAdapter.getView(mLastItemPosition,
getCachedView(), this);
addAndMeasureChild(newBottomchild, LAYOUT_MODE_ABOVE);
bottomEdge += newBottomchild.getMeasuredHeight();
}
}
/**
* Starts at the top and adds children until we've passed the list top
*
* @param topEdge
* The top edge of the currently first child
* @param offset
* Offset of the visible area
*/
private void fillListUp(int topEdge, final int offset) {
while (topEdge + offset > 0 && mFirstItemPosition > 0) {
mFirstItemPosition--;
final View newTopCild = mAdapter.getView(mFirstItemPosition,
getCachedView(), this);
addAndMeasureChild(newTopCild, LAYOUT_MODE_BELOW);
final int childHeight = newTopCild.getMeasuredHeight();
topEdge -= childHeight;
// update the list offset (since we added a view at the top)
mListTopOffset -= childHeight;
}
}
@Override
public int getFirstVisiblePosition() {
return mFirstItemPosition;
}
@Override
public int getLastVisiblePosition() {
return mLastItemPosition;
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
return childCount - 1 - i;
}
/**
* Adds a view as a child view and takes care of measuring it
*
* @param child
* The view to add
* @param layoutMode
* Either LAYOUT_MODE_ABOVE or LAYOUT_MODE_BELOW
*/
private void addAndMeasureChild(final View child, final int layoutMode) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
final int index = layoutMode == LAYOUT_MODE_BELOW ? -1 : 0;
// child.setDrawingCacheEnabled(true);
addViewInLayout(child, index, params, true);
final int itemWidth = getWidth();
child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED
| mChildHeight);
if (mChildHeight == 0) {
mChildHeight = child.getMeasuredHeight();
mScroller.setRange(mAdapter.getCount() * mChildHeight
+ (int) (mLayoutHeight * HEIGHT_FACTOR));
mScroller.setBounceRange(getHeight() / 2);
mScroller.setMaxFlingVelocity(2500);
}
invalidateChild(child);
}
/**
* Positions the children at the "correct" positions
*/
private void positionItems(boolean newLayout) {
int top = mListTop + mListTopOffset;
final int width = getWidth();
final int height = mChildHeight;
for (int index = getChildCount() - 1; index >= 0; index--) {
final View child = getChildAt(index);
if (newLayout || child.getHeight() == 0) {
child.layout(0, top, width, top + height);
}
child.setTag(string.tag_top, top);
top += height;
}
}
/**
* Checks if there is a cached view that can be used
*
* @return A cached view or, if none was found, null
*/
private View getCachedView() {
if (mCachedItemViews.size() != 0) {
return mCachedItemViews.removeFirst();
}
return null;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
mListTop = -firstVisibleItem;
requestLayout();
}
private boolean mSnap = false;
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == SCROLL_STATE_IDLE && mSnap) {
final View c1 = getChildAt(getChildCount() - 1);
final View c2 = getChildAt(getChildCount() - 2);
final int dist1 = (Integer) c1.getTag(string.tag_top);
final int dist2 = (Integer) c2.getTag(string.tag_top);
if (Math.abs(dist1) < Math.abs(dist2)) {
mScroller.scrollByDistance(dist1, 400);
OnItemSelectedListener listener = getOnItemSelectedListener();
if (listener != null) {
listener.onItemSelected(this, c1, c1.getId(), 0);
}
} else {
mScroller.scrollByDistance(dist2, 400);
OnItemSelectedListener listener = getOnItemSelectedListener();
if (listener != null) {
listener.onItemSelected(this, c2, c2.getId(), 0);
}
}
mSnap = false;
} else if (scrollState == SCROLL_STATE_TOUCH_SCROLL) {
mSnap = true;
}
}
public int getChildHeight() {
return mChildHeight;
}
public void invalidateChild(View child) {
child.setTag(string.tag_invalidate, true);
invalidate();
}
}