0

I would like to create simple custom UI elements in Android like the ones from the screenshot:

enter image description here

The light bulb should always have the same size but the rectangle should vary in the width. One option of doing this is to use Canvas elements. But I would like to ask whether there is also an easier approach for doing this. Is it possible to maybe only do this by using XML files? I would like to use these UI elements then in the LayoutEditor like e.g. a TextView where I can adjust the widht and height either in the XML layout file or programmatically.

Any idea how I can do that in an easy way?

Update: I tried the suggested approach from Cheticamp and I have the following code inside my Fragment:

public class Test extends Fragment implements Runnable {

    /*
    Game variables
     */

    public static final int DELAY_MILLIS = 100;
    public static final int TIME_OF_A_LEVEL_IN_SECONDS = 90;
    private int currentTimeLeftInTheLevel_MILLIS;
    private Handler handler = new Handler();
    private FragmentGameBinding binding;

    private boolean viewHasBeenCreated = false;


    public Test() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentGameBinding.inflate(inflater, container, false);
        getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        container.getContext();
        viewHasBeenCreated = true;
        startRound();
        return binding.getRoot();


    }

    public void startRound () {
        currentTimeLeftInTheLevel_MILLIS =TIME_OF_A_LEVEL_IN_SECONDS * 1000;
        updateScreen();
        handler.postDelayed(this, 1000);

    }
    private void updateScreen() {
        binding.textViewTimeLeftValue.setText("" + currentTimeLeftInTheLevel_MILLIS/1000);

        /*
        IMPORTANT PART: This should create a simple custom UI element but it creates an error
         */
        View view = new View(getActivity());
        view.setLayoutParams(new ViewGroup.LayoutParams(100, 100));
        Drawable dr = ContextCompat.getDrawable(getActivity(),R.drawable.light_bulb_layer_list);
        view.setBackground(dr);

        ConstraintLayout constraintLayout = binding.constraintLayout;
        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(constraintLayout);
        constraintSet.connect(view.getId(),ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID,ConstraintSet.BOTTOM,0);
        constraintSet.connect(view.getId(),ConstraintSet.TOP,ConstraintSet.PARENT_ID ,ConstraintSet.TOP,0);
        constraintSet.connect(view.getId(),ConstraintSet.LEFT,ConstraintSet.PARENT_ID ,ConstraintSet.LEFT,0);
        constraintSet.connect(view.getId(),ConstraintSet.RIGHT,ConstraintSet.PARENT_ID ,ConstraintSet.RIGHT,0);
        constraintSet.setHorizontalBias(view.getId(), 0.16f);
        constraintSet.setVerticalBias(view.getId(), 0.26f);
        constraintSet.applyTo(constraintLayout);
    }

    private void countDownTime(){
        currentTimeLeftInTheLevel_MILLIS = currentTimeLeftInTheLevel_MILLIS -DELAY_MILLIS;
        updateScreen();
    }

    @Override
    public void run() {
        if(viewHasBeenCreated) {
            countDownTime();
        }
}
}

Unfortunately, this code leads to a "java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.content.Context.isUiContext()' on a null object reference". It is thrown by the line View view = new View(getActivity());. Here is the complete error message:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.game, PID: 12176
    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.content.Context.isUiContext()' on a null object reference
        at android.view.ViewConfiguration.get(ViewConfiguration.java:502)
        at android.view.View.<init>(View.java:5317)
        at com.example.game.Test.updateScreen(Test.java:72)
        at com.example.game.Test.countDownTime(Test.java:91)
        at com.example.game.Test.run(Test.java:97)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Any idea what the problem is? Without the custom UI element the Fragment works fine.

VanessaF
  • 515
  • 11
  • 36

3 Answers3

2

Sure thing.

In this case a simple xml file like so would suffice. Let's name it something.xml inside the layout folder.

<LinearLayout ...>
  <ImageView ...>
</LinearLayout>

In another layout xml file you may just:

<ConstraintLayout ...>
  <include android:id="@+id/something"" layout="@layout/something" android:layout_width="70dp">
</ConstraintLayout>

See Reusing layouts

If you'd like to get a children you can always get them by using findViewById on your Activity or Fragment. If you're using Databinding or Viewbinding it just gets better: They'll appear as fields in the XBinding class that was generated out of the XML file


Hi VanessaF, going a little bit further with the clarifications you asked in the comments:

<include />

The <include /> tag is a special XML tag that we can use in our Android XML layout files to indicate that where we placed the <include/> we'd like it to be replaced by some other XML determined via the layout attribute inside the <include /> tag.

Here's an example:

Considering layout/example.xml

<TextView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:text="Hello!"/>

And considering layout/parent.xml

<LinearLayout 
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">

  <Button .../>
  <include layout="@layout/example"/>
  <ImageView android:drawable="@drawable/ic_send"/>
</LinearLayout>

Whenever I use R.layout.parent somewhere (for example in setContent from the Activity the view that would get generated would be as follows:

<LinearLayout 
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">

  <Button .../>
  <!-- PLEASE NOTICE THAT <include/> IS GONE -->
  <!-- AND HAS BEEN REPLACED WITH THE CONTENTS the specified layout -->
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Hello!"/>
  <ImageView android:drawable="@drawable/ic_send"/>
</LinearLayout>

Effectively re-using the layout without writing a full-blown custom view.

Notice: All attributes you specify inside the <include/> tag will effectively override the others specified inside the layout file. Let me illustrate this using an example:

Consider again layout/example.xml. Notice that this time the TextView will shrink to the size of the text both in height and width.

<TextView 
  android:text="Hello!"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  />

And consider the parent: layout/parent.xml. Notice that I am setting the attributes android:layout_width and android:layout_height.

<LinearLayout 
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">
  <include
    layout="@layout/example"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />
</LinearLayout>

In this case, when Android replaces <include/> for the contents of @layout/example it will also set android:layout_width="match_parent" and android:layout_height="match_parent" because they were specified on the <include/> tag effectively ignoring the original attributes set inside layout/example.xml (which were set to "wrap_content")

Some random IT boy
  • 7,569
  • 2
  • 21
  • 47
  • Thanks IT boy for your comment. But what do I have to put inside the `something.xml` file? – VanessaF Jan 09 '22 at 16:16
  • The XML code of the layout you want to reuse and set some defaults. Then you can overwrite the defaults on the `` tag – Some random IT boy Jan 09 '22 at 16:19
  • Thanks IT boy for your comment. But how exactly should the XML-code look like for such an element? What do you mean by "set some defaults" and what is the "include" tag? – VanessaF Jan 10 '22 at 18:14
  • Hi Vanessa, I included further explanation and some more illustrative examples that I hope you find useful – Some random IT boy Jan 10 '22 at 21:19
  • @Thanks for your answer random IT boy. I am trying to use the answer from Cheticamp. Still thanks for sharing your approach. Maybe I will consider using it for another application. I upvoted your answer. – VanessaF Feb 02 '22 at 18:06
  • 1
    No problem. Always go for the simplest piece of code as long as it's enough for your needs! – Some random IT boy Feb 02 '22 at 18:16
2

Use a TextView. The light bulb can be a left compound drawable. Set the background to a rounded rectangle shape drawable. This can all be specified in XML. See TextView.

This can also be accomplished with a LayerList drawable if text is not wanted. (The TextView solution also works without text - just set the text to "" or null.)

<layer-list>
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="5dp" />
            <solid android:color="#FF9800" />
        </shape>
    </item>
    <item
        android:drawable="@drawable/ic_baseline_lightbulb_24"
        android:width="48dp"
        android:height="48dp"
        android:gravity="left|center_vertical" />
</layer-list>

The layer list is set as a background to a simple View.

<View
    android:layout_width="250dp"
    android:layout_height="56dp"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:layout_marginEnd="16dp"
    android:layout_marginBottom="16dp"
    android:background="@drawable/light_bulb_layer_list" />

enter image description here

To create the View in code:

View view = new View(context);
view.setLayoutParams(new ViewGroup.LayoutParams(width, height));
Drawable dr = ContextCompat.getDrawable(context,R.drawable.light_bulb_layer_list)
view.setBackground(dr);
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • Thanks for your answer Cheticamp. But actually I don't want a textview but just a rectangle with an icon. There should not be any text – VanessaF Jan 10 '22 at 18:11
  • @VanessaF Use a TextView but set the text to null or "". That will give you what you want. You will need to set width to get the size you want. You could also just use a layer list. I will post something about that. – Cheticamp Jan 10 '22 at 18:52
  • Thanks for your answer and effort. I will try what you suggested and if it works I will accept your answer. – VanessaF Jan 10 '22 at 18:56
  • Thanks cheticamp for your answer. Can I also do this programmatically without having to specify a specific item in the XML layout file? The reason for that is that those elements will be created, deleted and moved dynamically. So they will not always be at the same position. – VanessaF Jan 13 '22 at 16:14
  • @VanessaF You can do it all programmatically. Just create a view with the values specified in the answer. The view can be added to an _ViewGroup_. You can also create the [LayerDrawable](https://developer.android.com/reference/android/graphics/drawable/LayerDrawable) for the layer list if desired. – Cheticamp Jan 13 '22 at 17:14
  • Thanks Cheticamp for your answer and effort. I really appreciate it. How can I create the view in Java with the values specified in the answer and why shall I add the view into a viewgroup? Further, what use is it to create a LayerDrawable? – VanessaF Jan 13 '22 at 17:43
  • I changed the answer with some Java to create the view with the background. I wouldn't create the layer-list in code, but leave it as an XML drawable. Usually, a _View_ has to be added to some _ViewGroup_ to be useful, but maybe you have another use. – Cheticamp Jan 13 '22 at 20:12
  • Thanks for your comment and effort. I tried you suggested approach for creating and inserting the view programatically. Unfortunately it yields an error message "java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.content.Context.isUiContext()' on a null object reference" thrown by the very first line of your suggested code `View view = new View(getActivity());`. Maybe I should mentione that I use the Single-Activity Multiple-Fragments approach. So your suggested code is executed in a Fragment and not in an Activity (maybe is the reason for the error?) – VanessaF Jan 22 '22 at 15:24
  • It looks like the activity is not yet available to your fragment. Make sure you are creating the view after `onAttach()` is called. If you do the creation in `onCreateView()` you should be OK. You should use the fragment context anyway. – Cheticamp Jan 22 '22 at 17:15
  • Thanks for your comment. Actually I use your suggested code inside an own method that is called at the (almost) very end of the `onCreateView()` method (just before `return binding.getRoot();`). For all other components that I have in the XML layout file, everything works fine but for your suggested code to create the View programatically, I get an error. Actually I have never used the method `onAttach()` in any of my fragments in any app and so far not experienced an error. Further, I don't understand what you mean by "You should use the fragment context anyway". How can I do that? – VanessaF Jan 23 '22 at 09:30
  • Can't say without seeing code or stack trace. You can also use `container.getContext()` in `onCreateView()` to get the context. – Cheticamp Jan 23 '22 at 13:30
  • Thanks for your answer Cheticamp. I updated the question which now includes my try to implement your suggested approach (which leads to an error message). Would you mind having a look into it? – VanessaF Jan 26 '22 at 18:05
  • @VanessaF The fragment is also a runnable. If you start the runnable before `onCreateView()` is called, you will have this problem since `updateScreen()` will be called before the fragment gets started. – Cheticamp Jan 26 '22 at 19:34
  • Thanks Cheticamp for your comment. I upated my code to make sure that the content of the `run` method is only executed after the ònCreateView()` method has been executed (by inserting a new boolean `viewHasBeenCreated`). However, this does not change the problem and I am getting the exactly same error as before using your suggested code for creating a customized UI element altough now the view has been created. I updated my code. – VanessaF Jan 27 '22 at 18:25
  • @VanessaF Post the stack trace. – Cheticamp Jan 27 '22 at 20:15
  • I just posted the complete error message – VanessaF Jan 27 '22 at 20:43
  • @VanessaF `getActivity()` is defined in `onCreateView()` since that is executing without crashing so, somehow the activity is being detached - maybe due to `setRequestedOrientation()` which, probably, doesn't belong in the fragment at all. I would rethink the internal workings of the fragment. – Cheticamp Jan 27 '22 at 21:03
  • Thanks Cheticamp for your answer and effort. I really appreciate it. In fact, when commenting `setRequestedOrientation()` out, the app does not crash. However, there are 2 problems. 1) Where else shall I set the `setRequestedOrientation()` to a landscape mode if not in the only Fragment itself, which should have the landscape mode? The rest of the app should have the normal mode 2) There is no custom UI element in the layout that I can see. I also tried to increase the size and width and changed the constraints but still it is not visible. – VanessaF Jan 27 '22 at 21:23
  • @VanessaF 1) Set the orientation in the activity before calling the fragment or add code to determine if the activity is attached or not (I think 1st option is better.); 2) The custom view is never added to the _ConstraintLayout_. Add it and if you still don't see it, use the _Layout Inspector_ to check if it is in the _ConstraintLayout_ but just not showing. That should be enough to debug. – Cheticamp Jan 27 '22 at 22:04
  • Thanks for your answer Cheticamp. Actually the view is now visible. However, I could not fix the screen orientation problem. I tried to use your suggested approach and set `getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);` in the menu fragment that calls the Fragment that should be in Landscape mode (I can't set the orientation directly in the activity as I have a sinlge activity multiple fragments approach). Unfortunately, I get exactly the same null pointer error message as before – VanessaF Jan 29 '22 at 10:51
  • This is really strange as I use the code `getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);` in many other fragments without any problems. Only in the Fragment with the custom view this problem occurs. Somehow the custom view interferes with the Screen orientation – VanessaF Jan 29 '22 at 10:57
  • @VanessaF That's because have the runnable that making an activity rerference. Try creating the view once in `onCreateView()` where you know you have the activity attached and reuse the view rather than recreating it. – Cheticamp Jan 29 '22 at 12:00
  • Thanks a lot for your answer and effort. Now it works. I upvoted and accepted your answer. I have a follow-up question about how to set the constraints to this custom view (or any other view) programatically. If you want to, you could have a look at it here https://stackoverflow.com/questions/70790999/how-to-contrain-a-view-programmatically-to-parent-in-constraintlayout. – VanessaF Jan 30 '22 at 08:02
  • There is a problem with your suggested solution. I use your approach in a constraint layout and animate the UI objects (such that it flows from left to right). Strangely, sometimes the symbol within the rectangle (lightbulb) disappears during the animation from the rectangle (while the rectangle is still visible and animated) and sometimes you just see the symobl (lightbulb) in the beginning without the rectangle and shortly after you also see the rectangle. Is there no way to have a UI element that is fixed as it is and can't be separated into multiple parts as in your solution? – VanessaF Jul 15 '22 at 16:45
  • That is a pretty standard type of drawable, so there shouldn't be any problem animating it. If you do have trouble animating it so it looks OK, you may want to consider posting a question about it. An alternative to the layer list that is a single entity would be a vector drawable or a nine-patch drawable. You could also go with your original idea of a custom drawable using a canvas. – Cheticamp Jul 15 '22 at 20:05
0

I suggest reading Custom View Components from the official Android documentation. In fact, you should become very familiar with this documentation for everything you do with Android apps.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
  • Thanks Code-Apprentice. I know that this can be done with custom views and canvas. But this is quite complex and the effort for implementing it is very high (I have dealt with some Custom Views and I would like to avoid them wherever I can). – VanessaF Jan 10 '22 at 18:13