Which LayoutManager for the animations of a 2048 game?
G

2

8

For training purposes, I am currently replicating the 2048 game. I got the logic and interaction done, but print (and simply refresh) it inside a single TextView which looks ridiculous, of course.

When thinking about how to build a UI for the game, I am uncertain which LayoutManager to choose. What I need to be doing in the game's 4 x 4 grid is to merge and add cells inside rows and columns with animation. It seems to me that:

  • GridLayout won't work since it is dealing with the full dataset (16 tiles in four rows/columns) when I need to be able to deal with single rows and columns. However, I am not sure about this. I have worked with GridLayout before, but never manipulated the number of items (add, remove) and never used animation with that manipulation. Is there a way of manipulating single rows and columns?

  • LinearLayout won't work since I get in trouble when I want to manipulate columns, but have four horizontal LinearLayouts, or rows, but have four vertical LinearLayouts.

  • RelativeLayout or ConstraintLayout would be possible - maybe. Here the trouble seems to be that I have to keep track of my 16 views, TextViews probably, and programmatically construct full layouts so I can tell animation what to do. Certainly viable, but challenging.

  • CustomLayout is a choice, but on which superclass should I build it and why?

Am I missing the easy solution? If not, what would be the most "natural" LayoutManager for my data structure (which is currently an array of 16 integers, but could easily be changed to a 4 x 4 array of integers).

Gader answered 22/9, 2017 at 8:15 Comment(0)
I
9

Update: Included demo of a tile appearing and two tiles being combined.

I suggest that you go with ConstraintLayout. It will allow you to position your views efficiently and can provide you with some easy animation. I have mocked up a quick sample below to demonstrate this approach.

Here is a video of the results:

enter image description here

The XML layout uses ConstraintLayout as the view group. The two boxes are simply text views but could easily be image views or another type of view.

The two boxes simply move vertically between horizontal guidelines at 24dp (gdln0) and 520dp (gdln100) for textView1 and 148dp (gdln25) and 520dp (gdln100) for textView2. These movement are animated using ConstraintSet and TransitionManager through a click handler attached to the "animate" button. Here is the XML followed by the code:

activity_main.xml

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.constraint.Guideline
        android:id="@+id/gdln0"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="24dp" />

    <android.support.constraint.Guideline
        android:id="@+id/gdln25"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="148dp" />

    <android.support.constraint.Guideline
        android:id="@+id/gdln50"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="272dp" />

    <android.support.constraint.Guideline
        android:id="@+id/gdln75"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="396dp" />

    <android.support.constraint.Guideline
        android:id="@+id/gdln100"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="520dp" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginStart="24dp"
        android:background="@android:color/darker_gray"
        android:gravity="center"
        android:text="2"
        android:textColor="@android:color/white"
        android:textSize="72sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/gdln0" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginStart="24dp"
        android:background="@android:color/darker_gray"
        android:gravity="center"
        android:text="4"
        android:textColor="@android:color/white"
        android:textSize="72sp"
        app:layout_constraintStart_toEndOf="@+id/textView1"
        app:layout_constraintTop_toBottomOf="@id/gdln25" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:layout_marginRight="16dp"
        android:text="Animate"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent" />
</android.support.constraint.ConstraintLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private int mSquareSide;
    private int mMargin;
    private int mBoardState = 0;
    private TextView mNewView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Resources r = getResources();
        mSquareSide = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, r.getDisplayMetrics());
        mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, r.getDisplayMetrics());

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ConstraintLayout layout = (ConstraintLayout) findViewById(R.id.layout);
                ConstraintSet newSet = new ConstraintSet();

                mBoardState = (mBoardState + 1) % 3;
                switch (mBoardState) {
                    case 0: // Just reset the board to its starting condition.
                        setContentView(R.layout.activity_main);
                        findViewById(R.id.button).setOnClickListener(this);
                        break;

                    case 1: // Move tiles down and insert new tile.
                        mNewView = new TextView(layout.getContext());
                        mNewView.setId(View.generateViewId());
                        mNewView.setBackgroundColor(getResources().getColor(android.R.color.darker_gray));
                        mNewView.setTextSize(72);
                        mNewView.setTextColor(getResources().getColor(android.R.color.white));
                        mNewView.setText("2");
                        mNewView.setVisibility(View.INVISIBLE);
                        mNewView.setGravity(Gravity.CENTER);
                        ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(mSquareSide, mSquareSide);
                        mNewView.setLayoutParams(lp);
                        layout.addView(mNewView);
                        newSet.clone(layout);
                        newSet.connect(mNewView.getId(), ConstraintSet.TOP,
                                       R.id.gdln0, ConstraintSet.BOTTOM);
                        newSet.connect(mNewView.getId(), ConstraintSet.START,
                                       ConstraintSet.PARENT_ID, ConstraintSet.START, mMargin);
                        newSet.clear(R.id.textView1, ConstraintSet.TOP);
                        newSet.clear(R.id.textView2, ConstraintSet.TOP);
                        newSet.connect(R.id.textView1, ConstraintSet.BOTTOM,
                                       R.id.gdln100, ConstraintSet.BOTTOM);
                        newSet.connect(R.id.textView2, ConstraintSet.BOTTOM,
                                       R.id.gdln100, ConstraintSet.BOTTOM);
                        TransitionManager.beginDelayedTransition(layout);
                        newSet.applyTo(layout);
                        mNewView.setVisibility(View.VISIBLE);
                        break;

                    case 2: // Move tiles up and combine two tiles.
                        newSet.clone(layout);
                        newSet.clear(R.id.textView1, ConstraintSet.BOTTOM);
                        newSet.clear(R.id.textView2, ConstraintSet.BOTTOM);
                        newSet.connect(R.id.textView1, ConstraintSet.TOP,
                                       R.id.gdln0, ConstraintSet.BOTTOM);
                        newSet.connect(R.id.textView2, ConstraintSet.TOP,
                                       R.id.gdln0, ConstraintSet.BOTTOM);
                        Transition transition = new AutoTransition();
                        transition.addListener(new Transition.TransitionListener() {
                            @Override
                            public void onTransitionStart(Transition transition) {
                            }

                            @Override
                            public void onTransitionEnd(Transition transition) {
                                mNewView.setText("4");
                            // Here you would remove the overlapped view
                            // with layout.removeView(View);
                            }

                            @Override
                            public void onTransitionCancel(Transition transition) {
                            }

                            @Override
                            public void onTransitionPause(Transition transition) {
                            }

                            @Override
                            public void onTransitionResume(Transition transition) {
                            }
                        });
                        TransitionManager.beginDelayedTransition(layout, transition);
                        newSet.applyTo(layout);
                        break;
                }
            }
        });
    }
}
Impassable answered 26/9, 2017 at 19:43 Comment(0)
F
3

I think I would go with plain drawing the whole thing without using any TextView or similar.

Let me explain you why.

Assuming you have a set of 16 TextView, which is the maximum that you can have in your grid, you should manage which one is visible and which one is not, hide it or not, move it inside a layout or not. This would take up many resources and if you're using a layout like the ones you've described, a "free" transition would not be possible too.

Assume you have two tiles and you want one to slide on the other and merge them. The only usable layout I can think of is RelativeLayout because it's the one with the most easily manipulable constraints.

You'll have to deal with every TextView though, managing the gradual transition and the overlap, while making Android consider ALL the stuff a TextView can do.

Now, you'll have to do all that movement/overlapping stuff anyway, so why don't you do that by drawing each tile? You'll have to write some more code to draw an hypothetic Tile class into a canvas (note that you might change your data structure a bit).


After you have your drawing mechanism you'll have to make the same stuff you would do with TextViews, but in a different, freer, way of thinking, with fewer constraints and by using less Android resources.

I know you were asking advice about some Layout to use and I'm proposing to draw the tiles by yourself, this might not be what you needed and if I bothered you with this long talk I apologise, but if you were asking for advice in a "wider" manner I think this is a good idea to keep in mind.
And it would be a great occasion to learn about 2d drawing on Android too!

Happy coding!

Fuhrman answered 22/9, 2017 at 14:50 Comment(4)
I have drawn in Android (and in JavaScript) before, although not massive animated junks of the screen. Therefore I am somewhat sceptical about your suggestion, for two reasons: complexity and performance. After all, views were invented in order not to be dealing with pixels all the time and my ad-hoc graphics' algorithms would hardly out-perform the built-in Android system of views, layouts and animations. So, I appreciate your attempt (+1), but am not convinced by it.Gader
I respect that. I hope you'll find what you need and I would ask you to share it here if you do, because I'm curious and open to simpler alternativesFuhrman
Just revisited my old question and realized that I never shared my solution. Basically, I went with the ConstraintLayout option. As it happens I just recently uploaded my code to GitLab. Please take a closer look at the refreshTile code.Gader
That's very lean. I like it, thanks a lot for sharing!Fuhrman

© 2022 - 2024 — McMap. All rights reserved.