0

In my Android app I created a View showing a field of 9x9 (see simplified screenshot):

enter image description here

Each field in the view is represented by an ImageView (showing pictures instead of rectangles). Some ImageViews will be deleted by setting them to null and setting visibility to View.GONE and new ImageViews will be created every time a new element is added to the field. This means I create lots of ImageViews in a short amount of time. The point is, the game runs great on my HTC One, but it's lagging after a while (I think when garbage collection is running) and then it stops lagging and works great again.

This brings me to the idea of using Pool's to manage my objects, e.g. by recycling "deleted ImageViews" and reuse them by setting the position and changing the image source. I'm developing for plain Android, but I think about using the LibGDX framework.

My question is: Would you have any suggestions on how to implement Pool's in plain Java/Android? I found this post interesting, thought I could go with the Apache Commons ObjectPool. Is there a better way on Android or LibGDX?

Note: The ImageView's are not at the same position each round. I move them around using the Tween Engine. However, the number of the image views is kind of constant. Means at some point of the game I do have 81 ImageView's (9*9) plus a few ImageView's for animating special events (maybe +10).

I would appreciate any advices/recommendations.

Best regards,

Jimmy

Community
  • 1
  • 1
jimmyp.smith
  • 178
  • 6
  • If I understand idea of your game in proper way, the grid with imageviews is static - which mean the number of imageviews is always the same, am I right? Why don't you just update imageView background or image instead of recreating them? Idea with the Pool isn't good in my opinion at all - it might be useful if you need to implement something like list or grid view that scrolls in some direction – MP23 Mar 26 '14 at 21:46
  • Sorry, I did not mentioned it yet, but I'm moving the ImageViews around (animated with the Tween Engine), so they do not have the same position in each round. Otherwise I would totally agree with you, that it would be smart to change image source only. Furthermore I do have some more ImageViews for animating merging of two cards. Hope that makes sense to you. – jimmyp.smith Mar 26 '14 at 21:54
  • Ok, then I suggest to make use of some kind of collection e.g List, every image view that is no more neccesary should go there, and if some new ImageView is neccessary then you should first check if your list doesn't contain one, and update its background and position.Of course if you use it, you should remove it from the list. Create new ImageView only if the list is empty. I think it one of the simples Cache mechanism, but it works properly in e.g ListView (reuse of row Views) – MP23 Mar 26 '14 at 22:02
  • Is there a big difference comparing your solution to pools? In that case I would always need to search for the "invisible" ImageView. Could work, I would need to try it. Could you post your suggestion as answer, please? I will implement that by tomorrow, to give it a try. – jimmyp.smith Mar 26 '14 at 22:07

2 Answers2

1

I had a similar issue and never was really satisfied with the performance. The tip of it all was when I expanded my playfield to 30*30 so I got 900 ImageViews. I tried many optimizations, but the performance and memory was high and unpredictable across devices.

So what I did is to create a custom View. Just one. This view then paints squares in a canvas. I was amazed that now I can have literally tens of thousands of squares (100x100) and performance is ultra-smooth.

I post here the skeleton of my view with all the crap removed for an inspiration, I strongly recommend you to follow this approach.

    /**
     * ChequeredView is a view that displays a 2D square matrix, where each square can be individually selected.
     * For high performance, It only uses one view regardless of the matrix size (everything is drawn in a canvas)
     * @author rodo 13 march 2014 <rlp@nebular.tv>
     */

    public class ChequeredView extends View {

        private final String TAG="ChequeredView";

        private static final int 
            DEFAULT_MATRIX_SIZE_COLS=20, 
            DEFAULT_MATRIX_SIZE_COLS=20, 
            DEFAULT_SQUARE_SIZE=100;

        private int mCols=DEFAULT_MATRIX_SIZE_COLS, 
                    mRows=DEFAULT_MATRIX_SIZE_ROWS;

        /* Save touch press */
        private int mTouchX=0, mTouchY=0;

        ///////////////// VIEW CODE

        public ChequeredView(Context context, AttributeSet attrs) { super(context, attrs); }
        public ChequeredView(Context context) { super(context); }

        /**
         * Report a size of your view that is: SquareSize * NUM_COLS x SquareSize * NUM_ROWS. You will paint it later.
         */

        @Override
        protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);

            // calculate optimum square size
            mStyleSquareSize=MeasureSpec.getSize(widthMeasureSpec) / mSquaresPerCanvas;

            // report total size
            setMeasuredDimension(DEFAULT_MATRIX_SIZE_COLS * mStyleSquareSize, DEFAULT_MATRIX_SIZE_ROWS * mStyleSquareSize);
        }

        @Override
        public void onDraw(Canvas canvas)  {
            render(canvas);
        }


        @Override 
        public boolean onTouchEvent(android.view.MotionEvent event) {

            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // I execute the action in ACTION_UP so I can put this inside a scrollview and touch doesn't interferre the scroll.
                mTouchX=(int) event.getX();
                mTouchY=(int) event.getY();
                return true;

            case MotionEvent.ACTION_UP:

                // only process touch if finger has not moved very much (if it has, it's a fling on parent)

                if ( isApprox(event.getX(), mTouchX, 5) && (isApprox(event.getY(), mTouchY, 5)) ) 
                        processTouch((int)event.getX(), (int)event.getY());

                break;

            }
            return false;
        };

        /**
         * Check if a value is close to another one
         * @param value Value to check
         * @param ref Reference value
         * @param threshold Threshold
         * @return true if |val-ref|<threshold
         */

        private boolean isApprox(float value, int ref, int threshold) {
            float result=Math.abs(value-ref);
            return (result<threshold);
        }

        ///////////////// VIEW METHODS


        public void setMatrixSize(int numx, int numy) {
            mRows=numx;
            mCols=numy;
            invalidate();
        }

        ///////////////// VIEW INTERNALS


        /**
         * Renders the whole squaremap
         * @param canvas
         */

        private void render(Canvas canvas) {
            if (canvas==null) return;
            for (int x=0; x<mCols; x++) {
                for (int y=0; y<mRows; y++) {
                    render_square(canvas, x, y);
                }
            }
        }

        /**
         * Renders one of the squares
         * @param canvas Canvas where to draw
         * @param nCol The column
         * @param nRow The row
         */

        private void render_square(Canvas canvas, int nCol, int nRow) {


            String text=null, transition=null;
            int delay=0;
            Paint paint=null;

            int cx=nCol*mStyleSquareSize, cy=nRow*mStyleSquareSize;

            canvas.save();
            canvas.translate(cx, cy);
            canvas.drawRect(mStyleSquareMargin, mStyleSquareMargin, mStyleSquareSize-2*mStyleSquareMargin, mStyleSquareSize-2*mStyleSquareMargin, paint);

            // this draws an square (I use vectorial squares with text rather than images, but just change drawRect to drawBitmap)
            // just change it for drawBitmap() to draw one bitmap

            canvas.restore();
        }

        /**
         * Process a touch on the map area
         * @param x raw x coordinate
         * @param y raw y coordinate
         */ 

        private void processTouch(int x, int y) {
            int nx=x/mStyleSquareSize, ny=y/mStyleSquareSize;
            mSelectedX=nx;
            mSelectedY=ny;
            if (mSquareListener!=null) {
                mSquareListener.onSquareSelected(nx, ny, data);
            } 
            invalidate();
        }
    }
rupps
  • 9,712
  • 4
  • 55
  • 95
  • Would you have any suggestion how I can use ImageView's with this skeleton? I do not paint rectangles - every ImageView is representing another image source (but some will be the identically). However, I can imagine than painting it again and again could be way faster. – jimmyp.smith Mar 26 '14 at 22:13
  • 1
    forget ImageView, just use drawBitmap where I indicated. drawBitmap can draw any bitmap on a canvas. ImageView internally is doing exactly this! Look at the function render_square! – rupps Mar 26 '14 at 22:14
  • yeah... the point is precisely to get rid of ImageView and instead of creating 400 views create just one. The performance boost is relative to these savings, trust me. – rupps Mar 26 '14 at 22:21
  • Now thinking about it, I hope it will work with the Tween Engine. The Tween Engine is moving all elements around (any kind of layout or element in the layout). I already implemented all animations using this engine. Right now, I have no clue if that will work with canvas. Hope so.. – jimmyp.smith Mar 26 '14 at 22:24
  • Hmm... that might make the things complicated.. didnt heard about tween engine. You move the squares? breaking the grid? If it's just an entry animation maybe you can apply an animation to the whole view – rupps Mar 26 '14 at 22:30
  • Yes I'm moving the squares/images. I've edited my question since @MP23 pointed that out. Btw. I gave it a +1 since I like your idea! Haven't heard about drawBitmap before.. – jimmyp.smith Mar 26 '14 at 22:32
  • the image you posted confused me then, I thought it was kinda Minesweeper. BTW would be cool if you post a screenshot, looks like a nice game. Oh, and if you change to LibGDX you will have to do it possibly very similar to my solution :) – rupps Mar 26 '14 at 22:34
  • I will do, when it reaches the Google Play store :D For now it's top secret (except the details I've mentioned) :-P – jimmyp.smith Mar 26 '14 at 22:36
  • 1
    hah I meant to understand your question, I have enough with my projects! – rupps Mar 26 '14 at 22:37
1

According to Jimmy's suggestion, I am posting my comment as the answer

I suggest to make use of some kind of collection e.g List< ImageView >,

  • every image view that is no more neccesary should go there

  • if some new ImageView is neccessary then you should first check if your list doesn't contain one, and update its background and position.

  • Of course if you use it, you should remove it from the list.
  • Create new ImageView only if the list is empty.

I think it is one of the simplest Cache mechanism, but it works properly in e.g ListView (reuse of row Views)

MP23
  • 1,763
  • 20
  • 25
  • Or I would use two lists.. one for visible ImageView's and one for invisible ImageView's. As I told you, I will let you know tomorrow. Can't wait to implement it :) – jimmyp.smith Mar 26 '14 at 22:16
  • It works great using two list views: `List cacheViews` and `List currentViews`. When `cacheViews` is empty, I create a new ImageView and store the reference in `currentViews`, if it's not empty I reuse a view from `cacheViews` by moving it to the other list, set position and visibility.. Recycling is easy, simply set visibility to `View.GONE` and put it in cacheViews. Thanks for the idea/reminder :-D – jimmyp.smith Mar 28 '14 at 08:57
  • good to hear that, rupps answer is also very good but it is compeletely different approach – MP23 Mar 28 '14 at 09:00
  • That's right, your solutions was easier to implement into my existing code and it works when I move the ImageView's around using the Tween Engine. Think that would be rather complicated with rupps solution. However, I will try something similar to rupps solution when I switch to the LibGDX framework, cause the framework uses the (continuous) render approach instead of having ImageView's. (At least it's how I understood it, so far) – jimmyp.smith Mar 28 '14 at 11:49