1

Background: I have previously asked this question on the similar topic on how to pass an instance variable from a Java class to it corresponding layout file in Android development. It turns out there is no easy way to do this, so the following is a follow-up question finding a good way to let the xml file and the class talk to each other.

Question: I am developing an game (nearly complete in Java Swing, now turning it into an Android app). It contains multiple levels, each with a differenly sized chess board and chess pieces - for all intents and purposes, it is a chess puzzle app that displays a new puzzle on a differently sized board after the player has solved the previous puzzle. The business logic is complete but I'm working on the graphics. At the game start, a static class BoardFragment (contained by BoardContainer.class) with its corresponding layout xml file fragment_board.xml should display the board size (as GridLayout) and chess pieces corresponding to the first level, which then updates as the level is completed. For clarification, the code looks somewhat like this (skip to the end to see my actual problem):

//Here is BoardFragment.class, static class contained by BoardContainer:

public static class BoardFragment extends Fragment {

     public BoardFragment() {
     }

     public int level=0;

     public void buttonPressed(View view) {
     //method that will advance the game and update "level" 
     //depending on which button was clicked
     }


         @Override
         public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_board,
                container, false);
         return rootView;
         }
     }

//This is fragment_board.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.exampletest.MainGame$BoardFragment" >

    <android.support.v7.widget.GridLayout
        xmlns:app="schemas.android.com/apk/res-auto"
        android:id="@+id/gridView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        app:columnCount="4" >

        <ImageButton
            android:id="@+id/imageButton1"
            android:layout_width="49dp"
            android:layout_height="49dp"
            android:contentDescription="@null"
            android:onClick="buttonPressed"
            android:src="@drawable/grid11" />

        </android.support.v7.widget.GridLayout>

</RelativeLayout>

Note that for simplicity and space, only one image button is currently in the xml file, but with app:columnCount equaling 4, there would be 16. In fact, the number should depend on the "level" instance variable in the corresponding class (so that if level==5 then perhaps app:columnCount==6 and so forth) but I'm not sure how I would commmunicate that from the class file to the xml. Is it possible at all? In fact, when the level is completed, both the board size and number of pieces should change. Should this be done by

  • Having one xml fragment file for each level? Then the previous question is solved, but for many levels, it would lead to many fragment files - is that best practice?

  • Having one fragment xml file that updates as the level is completed?

I appreciate any help.

Community
  • 1
  • 1
Sid
  • 563
  • 1
  • 10
  • 28

3 Answers3

2

As mentioned in the other answers, you will need to create the layout for your board dynamically at runtime. However, you can still define your individual Views and their attributes in XML layout resources, thus retaining the advantages they provide of separating the non-essential parts of the layout from the code and allowing automatic resolution of configuration-specific layouts.

For example, your layout could be adapted to separate layout resources for the board and cell layouts:

fragment_board_grid.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.exampletest.MainGame$BoardFragment" >

    <android.support.v7.widget.GridLayout
        android:id="@+id/gridView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

fragment_board_cell.xml

<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="49dp"
    android:layout_height="49dp"
    android:contentDescription="@null"
    android:onClick="buttonPressed"
    android:src="@drawable/grid11"
    tools:context="com.example.exampletest.MainGame$BoardFragment" />

These can now be dynamically inflated to create a board:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
    View rootView = inflater.inflate(R.layout.fragment_board_grid,
            container, false);
    GridLayout boardLayout = (GridLayout) rootView.findViewById(
            R.id.gridView);
    boardLayout.setColumnCount(columnsNum);
    boardLayout.setRowCount(rowsNum);
    for (int columnIndex = 0; columnIndex < columnsNum; columnIndex++) {
        for (int rowIndex = 0; rowIndex < rowsNum; rowIndex++) {
            ImageView cellView = (ImageView) inflater.inflate(
                    R.layout.fragment_board_cell, boardLayout, false);
            // Here you can customize it, add listeners, etc.
            boardLayout.addView(cellView);
        }
    }
    return rootView;
}

Upon starting the next level you will need to reinflate the board layout from scratch. You can approach this in two ways:

  1. Make each instance of your Fragment represent a particular board. This would make the Fragment logic extremely simple and focused on only one game level. The logic for starting a new level would be handled by it's Activity, by replacing the existing Fragment with a new instance. This would also have the advantage of making animations for starting a new level extremely easy to implement, as FragmentTransactions have inherent support for animations.

  2. Have the Fragment handle everything. This could be achieved by creating a generic method that would inflate a new board layout on demand and delegate to it from the onCreateView() callback, then implementing a method that would remove the current Fragment layout from it's parent and replace it with a fresh layout generated by the board-inflating method. The layout root can be gotten from the getView() method (or in case of the support library it's first child, as the library wraps the initial Fragment layout in order to block the View state from being saved by the Activity).

corsair992
  • 3,050
  • 22
  • 33
  • Thanks a lot, this is a top quality answer. I could not get this running in Eclipse though (eternal problems with "android.support.v7.widget.GridLayout" for a long time now, and now it can't locate the new .xml files for a reason that's beyond me). I am switching to Android Studio, I'll try this method there as soon as I get everything up and running. – Sid Jul 28 '14 at 23:45
  • The variable "cell" is undefined, did you mean "cellView"? Although then I seem to get a null pointer exception, not sure why. – Sid Jul 29 '14 at 16:41
  • @Sid: Yes, I meant "cellView" - I originally named it "cell", and forgot to update the reference when renaming it (I was writing the code directly in the posting input box). I have fixed that now. If you are getting a `NullPointerException` on _that_ line, it would mean that `boardLayout` is null, which would mean that you don't have any `View` with an id of "gridView" in the grid layout resource. – corsair992 Jul 29 '14 at 17:54
  • My GridLayout view in `fragment_board.xml` does have a "GridView" id. But the `root` in `root.findViewById` returns an error ("cannot receive symbol"), are you sure it belongs there? I removed it and added a `setContentView(R.layout.fragment_board_grid)` before so now the code compiles and runs correctly, although mysteriously I still get a "fatal error" (probably because this code is in a fragment?). If you know what the problem is, it would be nice, but no problem otherwise because then I'll pose another question on the network. – Sid Jul 29 '14 at 19:07
  • @Sid: Sorry, my mistake again - it should be "rootView" instead of "root" (fixed). `setContentView()` is used for setting `Activity` content not `Fragment`, so you should revert it to the original format if you are working inside a `Fragment`. – corsair992 Jul 29 '14 at 19:19
  • Thanks, now it runs (but for others following this answer: the line `xmlns:app="schemas.android.com/apk/res-auto"` is needed in the GridView in the xml file, at least on my computer). The app does not behave as expected however since the `CellView`s are lined up in a single row as opposed to filling out a board, for any values of `columnsNum` and `rowsNum` greater than one. Do you know why this happens? The app also crashes when going from portrait mode to landscape (and vice versa), but I hope that this problem won't happen if I insert copies of the xml files in a `land` and `port` directory. – Sid Jul 29 '14 at 23:58
  • 1
    @Sid: Yes, you would need to define the custom application namespace if you're going to assign support library `GridLayout`-specific attributes in the layout resource (you would need to add the "`http://`" scheme prefix though) - I just copied the XML from your question. I have added dynamic allocation of row/column sizes, which should fix the issue of the single-line layout (see my last edit to the code). I created a test app according to the code in my answer, and it's behaving properly for orientation changes - you should post a new question with details about the crash and the stack trace. – corsair992 Jul 30 '14 at 03:25
  • Yes that GridLayout error was definitely my fault, I'll update my question with that line. You've been most helpful. I will see if I can do something about the orientation error today and if not then I'll post a new question. – Sid Jul 30 '14 at 08:26
  • I figured out the error; I had removed the keyword `static` from the BoardFragment class when trying to make `setContentView()` work, and then I forgot to put it back (making it dependent on the BoardContainer class). Everything works now as expected, case closed! I am wondering about a few other things, such as how to define the layout differently in portrait and landscape, but first I will play around with listeners and so on before asking a new question on the network. Thank you. – Sid Jul 30 '14 at 19:19
  • 1
    @Sid: You should look around in the documentation for basic guides and references. For example, you can find out details about how to provide external resources for multiple configurations [here](http://developer.android.com/guide/topics/resources/providing-resources.html). You should also consider acquiring a good book on Android development if you're new to it - there are lots of new concepts involved, although you might not encounter most of them. Unfortunately, I can't give you any specific current recommendations, as it's been a long while since I started Android development. – corsair992 Jul 30 '14 at 19:51
  • I asked a follow-up question here: http://stackoverflow.com/questions/25100161/drawing-a-chessboard-in-android-with-a-custom-border-using-imageviews-and-gridl – Sid Aug 02 '14 at 23:46
1

You could always build your views dynamically in code based upon the level rather than having your views be static in nature via use of the xml file

For example...

//the layout you will work with
GridLayout layout = (GridLayout) findViewById(R.id.gridLayout);

//properties for button
Button btn = new Button(this);
btn.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); //this will depend how you want the view to look
btn.setText("My button created programmatically");
btn.setId(someId);

//add button to the layout
layout.addView(btn);

if(level == whateverlevel){ //pseudo code here obviously
    //make a new button/buttons in here like above and add it to your view depending on your level
}
Josh Engelsma
  • 2,636
  • 14
  • 17
  • Is it possible then to work with different screen sizes? Is this code snippet supposed to be in my onCreateView method in BoardFragment? – Sid Jul 27 '14 at 11:45
  • @Sid, yes you can still create views programmatically that work for different screen sizes by setting up the appropriate layout "params" and so on... Also, yes, this code would go in the onCreateView...however, I would recommend making yourself a helper method "createBoard(int level)" ...then call this helper in onCreateView and pass in the level number – Josh Engelsma Jul 27 '14 at 21:22
  • Great you've been most helpful. For anyone else reading this, I think this guide is good to get started: http://www.techotopia.com/index.php/Creating_an_Android_User_Interface_in_Java_Code. About setting different layout params for different screens sizes, how do I programmatically know the screen size? Is there some method that I can put in an if statement, like "isScreenSizeLarge()" or something? (With xml files, I can just have different folders like "large" or "xlarge" and the mobile will select the right one depending on its screen size). – Sid Jul 27 '14 at 22:16
  • @Sid: You would be better off defining your individual `View`s (as opposed to the whole layout) - or at least the dimensions - in XML layout resources and inflating them, instead of constructing them directly in your code. That way, you will be able to create a dynamic layout, while retaining all the native advantages of externalized resources. – corsair992 Jul 28 '14 at 00:39
  • @corsair992 That would be perfect, to get the best of both worlds. Is it possible that you can illustrate this with an example (maybe in a separate answer?) or link to a resource where this method has been used? At any rate, thanks for that suggestion. – Sid Jul 28 '14 at 09:50
  • @Sid: Ok, I have added an answer demonstrating how to generate the layout dynamically by inflating from individual `View`s defined in XML layout resources. – corsair992 Jul 28 '14 at 17:25
  • @Sid let me know if you still have any questions. Sorry I haven't responded, been at work all day – Josh Engelsma Jul 28 '14 at 20:24
1

A good way to handle your layouts dynamically is to generate them programmatically. It's not as easy as laying everything out in an XML file, but it will provide you with much more flexibility. Here is an example:

How to create a RelativeLayout programmatically with two buttons one on top of the other?

Community
  • 1
  • 1
Will Thomson
  • 875
  • 6
  • 19
  • Thanks, now I've been reading about this (it's complicated!). One question: when using xml, Android enables you to generate multiple layouts for each screen size. Is there a good way to do that with your method? – Sid Jul 27 '14 at 12:44