-2

我有一个我扩展的自定义 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();
    }
}
4

2 回答 2

2

由于调用notifyDataSetInvalidated()notifyDataSetChanged()没有解决问题,问题似乎出在您的StreamView自定义适配器视图类中。

于 2012-07-11T15:39:23.467 回答
1

首先,使用视图持有者。这将使您避免这段奇怪的代码部分:

for (int i = layout.getChildCount() - 1; i >= 0; i--) {
     View child = layout.getChildAt(i);
     if (child instanceof Gallery) {
          hlv = (Gallery) child;
          break;
     }
}

顺便说一句,你为什么用这种奇怪的方法来找到一个View

hlv = (Gallery)layout.findViewById(android.R.id.list)就足够了。

其次,您不应该创建新的 Gallery hlv = new Gallery(mContext),然后对其进行初始化并将其添加到您的布局中。这是低效且容易出错的。

您需要重用已经存在的画廊,您可以再次使用hlv = (Gallery)layout.findViewById(android.R.id.list)或(更好地)通过简单地访问视图持有者对象的字段来获得它。

于 2012-07-11T07:45:18.610 回答