私が拡張したカスタム AdapterView があります。これは、データを表示する Gallery ビューを含む RelativeLayouts をホストします。そのデータは、親アダプター ビューのアイテムごとに置き換える必要があります。
親 AdapterView のアダプター クラスの実装では
、リサイクルされた convertView 引数を取得するときに、getView() メソッドでネストされた Gallery のアダプターを変更する必要があります (親の adapterview の各項目は、異なるデータ セットに対応します。ネストされたギャラリー ビューに表示されます)。
このコードは、アダプター自体のデータ セットを内部的に変更しようとします。以前のデータセットには 3 つの項目があり、新しいデータセットには 1 つの項目があるとします。データセットの最初のビューを更新しますが、余分なビュー (削除する必要があります) をその場所に残します。余分な不要なビューにスクロールすることはできません (ギャラリーは、実際にはビューが 1 つしかない場合でも、ビューが 1 つしかないかのように動作します)。
スクリーンショット:
コード:
getView:
@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
}
カスタム AdapterView:
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();
}
}