Force gridview to draw all tiles
Asked Answered
U

4

5

I have an android gridview which i'm using some custom scrolling going on in, to let it scroll in two dimensions - this means that the default scrolling isn't called.

I suspect this may be the reason that the rows that are off-screen are invisible. I know they're there, they affect the layout and everything, but they never draw.

So my question is this - is there any way to force the gridview to draw all of its tiles when it's loaded, and not just the visible ones?

Thanks.

Edit: To clarify - In my tileadapter, i set the child count to exactly 225. In my gridview, a call to getChildCount() returns 165.

Edit again: This only happens when the height of the gridview is greater than that of the screen - the children that are off-screen on the y axis are simply subtracted from the childcount - setting the size of the children to a number where they all fit snugly on screen removes the problem, but kills the purpose of scrolling.

Code!

XML Layout of activity:

  <LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:theme="@style/Theme.Custom"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <TextView android:id="@+id/logmessage"
  android:theme="@style/Theme.Custom"
  android:layout_width="fill_parent"
  android:layout_height="25dip"
  android:text="LogMessage"/>

  <RelativeLayout android:id="@+id/boardwrap"
  android:layout_weight="1"
  android:layout_height="fill_parent"
  android:layout_width="fill_parent"
  android:gravity="center_vertical">
  <com.MyProject.GameGrid 
    android:id="@+id/board"
    android:theme="@style/Theme.Custom"
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content"
    android:numColumns="15"
    android:stretchMode="none"
    android:verticalSpacing="0dip"
    android:horizontalSpacing="0dip"
    android:padding="0dip"
    android:columnWidth="20dip"
    android:scrollbars="none"/>
</RelativeLayout>
<RelativeLayout 
    android:id="@+id/toolbar"
    android:layout_width="fill_parent"
    android:layout_height="60dip"
    android:background="#FFFFFFFF"/>
</LinearLayout>

Activity:

public class GameBoardActivity extends Activity {

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.gameboard);

        GameGrid Board = (GameGrid)findViewById(R.id.board);
        Board.setAdapter(new TileAdapter(this));
    }
}

GameGrid:

public GameGrid(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.setNumColumns(15);

        DisplayMetrics metrics = new DisplayMetrics();
        ((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(metrics);
        scale = metrics.density;
        smallSize = Math.round(20 * scale);
        largeSize = Math.round(40 * scale);

        columnwidth = largeSize;
        this.setColumnWidth(columnwidth);
        Common.DebugMessage(Float.toString(columnwidth));

    }

You may notice i'm defining a small and a large size here - double tapping the screen allows you to switch between the two.

Scrolling (what you helped me with earlier)

if (myState == TOUCH_STATE_SCROLLING) {
                    final int deltaX = (int) (mLastX - x);
                    final int deltaY = (int) (mLastY - y);
                    mLastX = x;
                    mLastY = y;

                    int xpos = this.getScrollX();
                    int ypos = this.getScrollY();

                    int maxX = (columnwidth * 15) - super.getWidth();
                    int maxY = (columnwidth * 15) - super.getHeight();

                    if (xpos + deltaX >= 0 && xpos + deltaX <= maxX && ypos + deltaY >= 0 && ypos + deltaY <= maxY )
                    {
                        this.scrollBy(deltaX, deltaY);
                    }
                    else {
                        this.scrollTo(xpos + deltaX <= 0 ? 0 : xpos + deltaX >= maxX ? maxX : xpos + deltaX,
                                      ypos + deltaY <= 0 ? 0 : ypos + deltaY >= maxY ? maxY : ypos + deltaY);
                    }
                    Common.DebugMessage(this.getChildCount());

                }

Common.DebugMessage is just a helper method for printing debug messages to LogCat

TileAdapter:

public TileAdapter(Context c) {
        mContext = c;
    }

    @Override
    public int getCount() {
        return 225;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ImageView imageView;
        int colWidth = ((GameGrid)parent).getColumnWidth();
        if (convertView == null) {
            imageView = new ImageView(mContext);
            imageView.setLayoutParams(new GridView.LayoutParams(colWidth , colWidth));
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            imageView.setPadding(0, 0, 0, 0);
        }
        else {
            imageView = (ImageView)convertView;
        }
        imageView.setImageResource(R.drawable.tile);
        return imageView;
    }
Uriia answered 15/10, 2011 at 6:10 Comment(4)
Andreas, something JUST occured to me.... Try adding a myGridView.invalidate() to the end your ACTION_MOVE: in your onTouchEvent() (after the scrollBy() statement....)Crossruff
I've tried that before - nothing happens. Calling getChildCount after that results in the same number (165), though running the app on a device with less vertical space decreases that number to visible rows * 15.Uriia
Andreas, when your bounty runs out, I will be re-offering a higher bounty so we can get more eyes on this. This way it does not get wasted.Crossruff
I awarded you the bounty - you've earned it. I'm at work at the moment, but I'll continue testing later this evening. Thanks for all your hard work.Uriia
I
4

Honestly, my suggestion is to stop whacking on a platform API in a way that it is clearly not intended to be used. Even if through some contortions you managed to play enough games with the base GridView code to make it do what you want to do... how confident are you that your code will continue to work with the slightly changes to the GridView implementation as the platform evolves.

And really there is just no need to play these kinds of games. There is nothing special about GridView -- it is just an implementation of a view that puts things in a grid that you can scroll horizontally through?

If GridView's behavior is not what you want, the solution is to write your own view that does what you want. And with Android this is even easier because you can just go and take the GridView code from the open-source platform as a basis, get that compiling in your app (you will probably need to tweak a few things because the code as it stands takes doesn't need to be written purely against the SDK so probably isn't... but there is nothing it is doing that you can't do in a regular app build against the SDK), and then modify that code in your app to your heart's content, to make it do what want. Without fighting with a built-in widget that doesn't actually do what you want. And without fear of your carefully constructed house of cards collapsing on you if the underlying GridView implementation changes in the future.

Instancy answered 31/10, 2011 at 3:10 Comment(0)
C
5

Andreas,

If your issue is simply an onDraw() issue. You may do so quite easily with an overridden Draw(canvas) in your GridView. This has the side effect of increasing processor requirement while your Activity is loaded, but can create the desired effect. Such an override would be as follows:

 //This may be used in your GridView or Activity -- whichever provides the best result.
    public void draw(Canvas canvas)
    {   int _num = myGridView.getChildCount();
        for (int _i = _num; --_i >= 0; )
        {   View _child = (View)myGridView.getChildAt(_i);
            if (_child != null)
                _child.draw(canvas);
        }
    }

An Additional Technique (EDIT)

Sometimes overriding the draw() can have adverse effects. What we're really trying to do is trigger the objects to draw when they are available to. Overriding invalidate() in a similar manner can often have an effect depending on where the issue lies. Since we've established that we are getting strange results with overriding draw(), this seems to be the next course of action.

//This definitely goes in your GridView
public void invalidate()
{   int _num = myGridView.getChildCount();
    for (int _i = _num; --_i >= 0; )
    {   View _child = (View)myGridView.getChildAt(_i);
        if (_child != null)
            _child.invalidate();
    }
}

It has to be understood that this technique may not force a draw when you want, but merely lets the OS know that it is ready to be redrawn when it is available. Because invalidation does not automatically cascade down the View tree, if your ImageViews are nested deeper than the GridView you will have to adjust. This can be used in conjunction with the draw() or independantly. Additionally, when used with the appropriate placement of an invalidate() statement, may lower response but should help to keep your images drawn.

The issue may simply be a delayed layout or draw issue. If that is the case, a Lazy Loading solution might be best. A lazy loader is a way to load the content when it is needed, so that it typically uses less memory, and processing, and shows what is needed, when it is needed. Now I'm not awesome at Lazy Loading because I rarely have the need. But there is a GREAT example of code at this site. It is tailored for a GridView, as well.

Deemed Not Applicable (but may be useful for others)

The only other kind of issue that I think it may be is an Out of Memory issue that isn't causing a Force Close (and yes, they do exist). These are especially elusive and painful to take care of. The only way to spot these is in your LogCat during the load or scroll, with the error view selected. Or you can go through an entire LogCat dump (I recommend clearing the Log before you run your app first).

With this kind of issue, it becomes important to know how the lifecycle of an image works. If your images are being shrunk to thumbnail size in order to display, I would consider making real thumbnails alongside the fullsize images. Because shrinking an image in runtime temporarily requires more memory than keeping it the original size. Since this is all happening in one fell sweep, this may be a temporary problem, exhibiting itself permanently. This will dramatically lower memory requirements. (As a for instance, a 2x2 image requires 16 Bytes plus the Header, whereas a 4x4 image requires 64 Bytes plus the Header [400% for double the size!!].)

Additionally, adding System.gc() to critical places in your code will force a garbage collection, freeing up more memory, more often. (This is not a guarantee to free memory, but works more often than it doesn't).

The BEST solution would probably be a combination of all three, but would require a little more information than what we have with this question. For instance, we would need to see if you have overridden draw() and onMeasure() and onLayout() and maybe some other details.

Crossruff answered 19/10, 2011 at 7:10 Comment(10)
Good man, i'll try all of your suggestions out. I know for sure the problem isn't an out-of-memory exception. I have tried the program on a plethora of android devices, and the problem is always the same - the rows that are not visible when the gridview is first loaded does not draw, doesn't matter how many of them are off-screen. I'll upload an illustration shortly.Uriia
As for the other things - i haven't overriden draw, onMeasure or onLayout. I am however doing my coding in a subclass of GridView, and not the activity, if that makes any difference. I also just tried out the overridden draw-method you supplied, with to say the least interesting results - this.getChildCount() returns 165 (consistent with the number of children on screen at loadtime), when the actual number is 225. Also it only draws the first tile when overridden, for some reason, even though it does call draw on all the children.Uriia
That is indeed interesting... It should draw ALL of the children (with no regard to viewport). OK, so its not Out of Memory. It is definitely a drawing issue. That's good to know... Since your code is largely in your GridView, I need to ask... was the draw() code in the GridView or the Activity? I ask because WHERE your draw() can have a large impact on WHAT is drawn. I was surprised to learn that a child View is not invalidated when a parent View is like in some other languages, for instance. P.S. I hate draw issues... ROFLCrossruff
I just tried overriding invalidate, the problem with the childcount only being 165 persists, and the only result of the override was artifacts all over the screen :(Uriia
There is something that we are missing here. Is there perhaps a link to your code? I would need to see your TileAdapter, your GridView, the XML (if any for the GridView and the Tiles). The above are all standard techniques and one or more of them should have provided some better results.Crossruff
We are on the right track, because the fact that you are getting artifacts means that it is TRYING to draw them (otherwise it would just be blank and empty).Crossruff
Well, what i'm getting is fragments of the images that ARE visible when i'm scrolling. Like when windows locks up on you, and you drag a window around and every position it's been in continues drawing. Or when you win at solitaire and the cards create trails. I'm still only getting 165 children when calling childCount. And after inspecting my code, i found that my claim that the invisible tiles 'affect the layout' may have been an untruth, as what i meant was that the scrolling stopped where it should, an effect of my own scrolling logic (maxY being current tile height * 15 - view height)Uriia
I added just silly amounts of code, if you can make any sense of it i'd be most obliged.Uriia
Andreas, I KNOW that bug (exactly). It happens whenever specific views that are bigger than their environment have nothing to draw on top of. There are a number of ways to solve the problem. Let me look at your code and see what the most viable is for your program. (It's probably one of the most common issue for Home Launchers to overcome ). The reason my program has never exhibited it with my GridViews is because I fixed the bug with my Home Screens. LOL.Crossruff
If you want to research an appropriate answer for yourself, you can look up the source for ADW Launcher by AnderWeb (he keeps it on github). In the many many lines of code, there is something for an live wallpaper (lwp) support/wallpaper hack. That's a decent start to your answer. I'll find a more specific one if you give me a day or two. (I don't use his hack and solved it in my own ways. In other words, I was not aware EXACTLY what its cause was, but now I do)Crossruff
I
4

Honestly, my suggestion is to stop whacking on a platform API in a way that it is clearly not intended to be used. Even if through some contortions you managed to play enough games with the base GridView code to make it do what you want to do... how confident are you that your code will continue to work with the slightly changes to the GridView implementation as the platform evolves.

And really there is just no need to play these kinds of games. There is nothing special about GridView -- it is just an implementation of a view that puts things in a grid that you can scroll horizontally through?

If GridView's behavior is not what you want, the solution is to write your own view that does what you want. And with Android this is even easier because you can just go and take the GridView code from the open-source platform as a basis, get that compiling in your app (you will probably need to tweak a few things because the code as it stands takes doesn't need to be written purely against the SDK so probably isn't... but there is nothing it is doing that you can't do in a regular app build against the SDK), and then modify that code in your app to your heart's content, to make it do what want. Without fighting with a built-in widget that doesn't actually do what you want. And without fear of your carefully constructed house of cards collapsing on you if the underlying GridView implementation changes in the future.

Instancy answered 31/10, 2011 at 3:10 Comment(0)
C
0

I've submitted another answer instead because after clarification, the issue is definitely different from what I initially believed. The techniques in the previous answer should not be forgotten, however, and are a great resource for other related issues, so I'm keeping it there.

Problem:

The GridView now draws (kinda), but is repeating the same images over and over again, resulting in horrible artifacts. The resulting artifacts are so bad, that it doesn't really give a good indication as to whether the other views are even present. This issue is actually caused by an issue with "too much transparency".

Android and Transparency

Android does a wonderful job handling transparency, allowing objects below the current view to be drawn while the top view is in focus. However, if all of the Views are transparent, then there is nothing for Android to draw behind everything when it needs to refresh. Ordinarily, this is not an issue.

Generally, developers use the tools provided us and just try and make some neat things happen with them. (YAY!) And as long as we use them, Android says (pretty much) "I know what to do!!" When we begin with custom Views, however, Android can get a little freaked out, especially when it comes to drawing appropriately.

Your GridView is not actually a GridView. It is an extended GridView. As an extension of an Android View, Android makes no assumptions about how it should be drawn, soooo.... As a result, the normal opaque background for your GridView does not exist. One would think, "but I have it in a RelativeLayout. Isn't that enough?" The answer is no. Layout objects are Layout objects. They have no background unless we specify them.

This becomes exacerbated when you do Scrolling and other similar movements or animations. In these cases, Android tries to cache. The caching happens kind of below all of the Views, but the result is that since it is all transparent, you see the whole process. And it doesn't reset until it needs to clear the cache. This is why is gets uglier and uglier.

In other words, the "Window" may appear black, but is not actually black...

Solution (Part I):

So the answer is to set an opaque background to either the extended GridView or to one of its parent Views. This should resolve the artifacts you are seeing when you scroll. And it should allow you to see properly how and if the other views are rendering. Ultimately, you want to try and apply the background to the topmost View that encompasses all Views. The background can be as simple as setting the background color (as it creates an opaque drawable).

Next Steps:

I've noticed you have omitted some pertinent code. I really need to know what kind of Adapter your TileAdapter extends and how it gets its base information. This can have an impact on how it is gaining and appropriating the draw events. Once we know that, we can work on fixing the rest.

I also need to know how you are positioning the Tiles (there is no positioning code). Since there is not positioning code, there is no way to know whether the Views are actually being added. The default behavior for ImageViews is that if at least some part of them aren't visible, they don't get added to the hierarchy until they are. We want to force that issue

The end result may still require adjusting layout, measures or draws, but we won't know until we get an accurate representation of what is happening.

An Alternative Result

Something, I wish happened with scrolling games more often is if they started with a zoomed out position and we could "move" to the zoomed in position.. This could easily resolve your ChildCount, as you said if it fits neatly on the screen, they all draw. And the initial zooming in could happen after everything is all loaded. Then you have a nice graphical effect indicating load is complete. A Zoom out is just a normal animation, so easy to implement. and you know all of your children are loaded. Additionally, it may limit the code you have to enter in order to get it working correctly. Finally, it can all be done with the same GameGrid object. :)

I don't know if you thought about this, but I figured, "hey, this seems like the easy way to kill more than 2 birds with one stone."

Crossruff answered 26/10, 2011 at 2:49 Comment(4)
Hello, and thanks again for you interest in my problem. One thing i can answer right now is that the TileAdapter extends BaseAdapter. Starting with the grid "zoomed out" doesn't do a whole lot, i can see all 225 children, but as soon as i zoom in, it reverts back to 165 children. As you may have noticed, my zooming is actually just a doubling of the size of all the children. You haven't seen any position code, because there isn't any. I was trusting the GridView to handle that part, because otherwise i might as well be using a canvas or something.Uriia
Awesome. That's absolutely fine. I just needed to know what was going on, if anything, in botn of those areas. So, when you zoom in, those children remove themselves from the hierarchy?Crossruff
I suppose so, they were 225 before, and when zoomed in, the rows that can't fit on screen are removed.Uriia
Are you just doing a simple animation for your zoom? Or are you redrawing the Grid?Crossruff
C
0

override getCount() of your adapter and return the length of underlying array for your adapter.And then use mAdapter.getCount instead of mGrid.getChildCount. getChildCount returns only the visible children whereas, getCount would give you total children in your dataset.

Cutting answered 16/2, 2012 at 9:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.