Sundraw
A Mobile Developer's Blog
Monday, 11 January 2016
Why Xamarin?
Friday, 16 January 2015
Animated Accordion for Android 4+. Part 1: Static Version
In the company where I work, designers strongly favour accordion-like interfaces, so I have to implement them in mobile apps time and again. On iOS, it is all about inserting cells into UITableView or removing them — a process that can become quite tedious, especially when more than one level of accordion is needed. On Android, however, creating an accordion doesn't take much effort.
- Most importantly, it is not animated, changes happen abruptly.
- You are limited to a two-level list.
- Arguably, (I am not an expert in the usage of this particular component but I saw a fellow developer struggling with it) you might not have sufficient control over which section is open and when.
I created an Android Studio project and checked it into a Github repository. I am going to tag that code as we go along.
At first, let's create the simplest possible, static version of the accordion. It can be useful in those cases when the whole structure of the accordion is known at the outset. The key thing here is the layout file, which is
activity_main.xml
in the initial project:<ScrollView 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=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" android:orientation="vertical" > <TextView android:id="@+id/header1" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_green_dark" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/section_1" android:textColor="@android:color/white" android:textSize="33sp" /> <LinearLayout android:id="@+id/section1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_11" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_12" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_13" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> </LinearLayout> <TextView android:id="@+id/header2" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_green_dark" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/section_2" android:textColor="@android:color/white" android:textSize="33sp" /> <LinearLayout android:id="@+id/section2" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_21" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_22" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_23" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> </LinearLayout> <TextView android:id="@+id/header3" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_green_dark" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/section_3" android:textColor="@android:color/white" android:textSize="33sp" /> <LinearLayout android:id="@+id/section3" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_31" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_32" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_33" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> </LinearLayout> </LinearLayout> </ScrollView>At the root is a
ScollView
, to make sure that the content can scroll when necessary. It contains a vertical LinearLayout
which, in its turn, contains three TextView
s, for section headers, and three nested vertical LinearLayout
s, for section contents. The essential details are that the main LinearLayout
has the animateLayoutChanges
attribute set to true, and that the visibility of all nested LinearLayout
s is set to GONE
.Here is the code that manipulates this layout:
View section1, section2, section3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); section1 = findViewById(R.id.section1); section2 = findViewById(R.id.section2); section3 = findViewById(R.id.section3); View header1 = findViewById(R.id.header1); header1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (section1.getVisibility() == View.GONE) { section1.setVisibility(View.VISIBLE); } else { section1.setVisibility(View.GONE); } } }); View header2 = findViewById(R.id.header2); header2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (section2.getVisibility() == View.GONE) { section2.setVisibility(View.VISIBLE); } else { section2.setVisibility(View.GONE); } } }); View header3 = findViewById(R.id.header3); header3.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (section3.getVisibility() == View.GONE) { section3.setVisibility(View.VISIBLE); } else { section3.setVisibility(View.GONE); } } }); }The code is so simple that I doubt it needs any comments. Run the app, try tapping on the section headers and see how the sections expand or collapse as appropriate. Here is how it looked on my Nexus 4, with only the first section open:
At the moment, you can expand all three sections. Can you change the code so that when one of the sections expands two other collapse?
The more realistic scenario is when the sections are known but their content has to be created dynamically — perhaps because it has to be retrieved from the server. I will show how I am dealing with that scenario in the next post.
Sunday, 9 June 2013
SherlockActivity and AlertDialog Don't Work Together
There is a catch with Android: many desirable new features were introduced to it starting from version 4, Ice Cream Sandwich, which was introduced in October 2011, more than a year and a half ago. However, as of the beginning of June 2013 there are still more than 40% of devices out there using the older, pre-4.0 versions of Android.
This is really a pain, because every developer would like to use the new advanced features of Android, but doing that he will lose 40% of the potential audience, which is clearly not acceptable.
Google made some effort to help us and provided a compatibility library with backports of many of the new features to the earlier versions of the platform. However, on some reason that library doesn't offer any solution for the ActionBar, while most developers these days would wish to use the ActionBar, and they would want their app to look consistently across all the different versions of Android.
Thankfully, a third-party library ActionBarSherlock addresses the problem, and it is well implemented and reliable, so everyone I know in the Android development world uses it.
Quite naturally, BCG recommends the AndroidBarSherlock and uses it in most examples. And that's fine. I followed the advice and now all the activities in my new app are SherlockActivities.
Everything was great until I needed an AlertDialog. I looked into BCG, and it recommends to use "the modern way to display the dialog" by using a DialogFragment. So I did that, created an implementation of the DialogFragment, and since I am targeting pre-ICS versions of the platform, I used the support library version of the fragments (android.support.v4.app.DialogFragment).
Everything went smoothly until I tried to display my new dialog. The incantation for that is
new SampleDialogFragment().show(getSupportFragmentManager(), "sample");
Unfortunately, my SherlockActivity had no idea how to get a fragment manager from a Google support library. And naturally, Google pretends that it has no idea of the ActionBarSherlock. So DialogFragment simply doesn't mix with ActionBarSherlock.
I checked the Android documentation, and it is all about the DialogFragment, as if other options never existed at all. Which is understandable, they did provide a backport of fragments for the older platforms, and the DialogFragment solution does offer some benefits compared to the old way in the situation when the device is rotated. Well, in the ideal world either the support library would offer an ActionBar backport, or everyone would be using Android 4.* by now. But in the real world we have to use ActionBarSherlock for now.
What unpleasantly surprised me though is that Mark Murphy, who in his BCG almost insists on using ActionBarSherlock, follows Google in offering only the DialogFragment solution, without even hinting that you have no way to use it with ActionBarSherlock.
Anyway, here is the solution. I had to dig it out from my old code, written more than three years ago:
AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.title)) .setMessage(getString(R.string.message)) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Handle positive button } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Handle negative button } }).show();
Thursday, 9 February 2012
Sencha Touch 2 + PhoneGap = Blank Web Browser Window Opening On An Android 3.2 Tablet
I was trying to make a Sencha Touch 2 app with some CSS3 animations in it performing reasonably on the Galaxy Tab with Android 3.2. When running the app in the Android web browser there were two major problems: the view was flashing after every transition quite unpleasantly and some bars of the chart were appearing/disappearing randomly, there seemed to be some problem with the page refresh.
I tried creating a simple native Android app and running the Sencha Touch app in a naked WebView. That didn’t work at all, the native WebView is more or less useless without an additional tweaking.
Finally, since I wanted to have an APK as an end result anyway, I’ve wrapped the app in PhoneGap 1.3. Immediately, it started behaving much-much better, the old problems were gone and the performance of the animation was close to native, and close to that on the iPad.
All this at the cost of a new major problem though: the very first interaction with the app after it had started was always causing a blank web browser window to appear, with about:blank URL in it. Since this window covered the app, the impression was like the app suddenly disappeared, like if it had crashed. The app was running just fine though, and after retrieving it from underneath the web browser one could appreciate all the benefits of PhoneGap wrapping.
While I was investigating this problem, PhoneGap 1.4.1 was released, but it didn’t fix the ‘about:blank’ problem.
I was able to find the reason for the about:blank window in the source code of PhoneGap. It is a minor bug that wouldn’t matter in most normal circumstances. There is a bigger problem that causes the minor bug to manifest, and I still don’t know whether that mysterious bigger problem caused by Sencha Touch or by Phone Gap. However, I fixed the minor problem, created a custom PhoneGap build, and everything works just fine now.
Below, I will describe the details of my investigation. I will also submit this discourse to Phone Gap in a hope that they will fix the bug in one of the coming releases.
I was debugging on a device, reproducing the problem and checking what LogCat says at that time. The message that corresponded to the blank browser window appearance was quite obvious:
I/ActivityManager(80): Starting: Intent { act=android.intent.action.VIEW dat=about:blank cmp=com.android.browser/.BrowserActivity } from pid 404
This means that some process has sent to Android an Intent requesting to view a specific URL, and the URL was specified as “about:blank”. The natural Android’s reaction to such a request is to start its web browser and to display in it a blank page. The next question was: which exactly code sends such an Intent? I obtained the source code of Phone Gap and searched through it. There is only one place in the code where such an Intent is created, in DroidGap.java
:
public void onDestroy() {
...
// Load blank page so that JavaScript onunload is called
this.appView.loadUrl("about:blank");
...
}
A question that could be asked here is why the onDestroy()
method of the DroidGap activity is invoked at the first interaction with the app, and this is exactly where the mysterious bigger problem manifests itself. Since the app continues to run successfully, I guess that another instance of DroidGap is being quietly created and destroyed behind the scenes. It might have been totally unnoticed if not a minor bug that I had discovered, so let me concentrate on the bug for now and report the other findings later.
The problem with PhoneGap is that the above line of code doesn’t actually achieve what it is expected to achieve according to the comment, i.e. it doesn’t load a blank page into this.appView
, where this.appView
is a reference to the instance of the WebView that is used by PhoneGap to display the app.
This is because the appView
has a WebViewClient attached to it (GapViewClient or CordovaWebViewClient depending on the version of the source code). That client has the shouldOverrideUrlLoading()
overriden, which means that each time the WebView is about to load a URL, that method is invoked first. If the method returns false
, the WebView proceeds with loading whatever URL was given to it, otherwise it does nothing and it is up to the developer to do any appropriate coding to react to the URL.
If we look inside the shouldOverrideUrlLoading()
method, we’ll see that it checks for some special cases, like URLs beginning with ‘mailto:’, ‘sms:’ and so on, but ‘about:’ isn’t one of those special cases. For those URLs that aren’t special, the WebViewClient does the following:
// All else
else {
...
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
ctx.startActivity(intent);
...
}
return true;
Basically, it creates an Intent, attaches to it the URL (which the WebView was intending to load) and throws the Intent to the Android OS to handle (and so open a blank browser window). Then it returns true
which tells the WebView something like “don’t bother handling this URL, I’ve already done everything that was needed”.
So as a result of this line of code:
this.appView.loadUrl("about:blank");
WebView does nothing while WebViewClient passes “about:blank” to Android to handle. Thus the developer’s intent to “load blank page so that JavaScript onunload is called” is circumvented, and this is a bug.
To fix the bug, I’ve added to shouldOverrideUrlLoading()
the following one-liner:
else if (url.startsWith("about:"))
{
return false;
}
So that for a URL like “about:blank” WebViewClient tells WebView: go on and load this URL, I don’t care.
I’ve created a custom PhoneGap JAR with this patch and it works fine, the dreaded blank page doesn’t appear anymore.
Now a few thoughts on what’s going on behind the scenes, why a DroidGap Activity is being destroyed invisibly. Here is the complete logging that accompanies the appearance of the problem:
W/webview(404): Stale touch event ACTION_DOWN received from webcore; ignoring
D/DroidGap(404): DroidGap.startActivityForResult(intent,-1)
I/ActivityManager(80): Starting: Intent { act=android.intent.action.VIEW dat=about:blank cmp=com.android.browser/.BrowserActivity } from pid 404
W/WindowManager(80): Failure taking screenshot for (230x135) to layer 21010
When I tried to find out which code could be calling startActivityForResult()
on DroidGap, I’ve found myself digging into the Capture
class and methods like captureImage()
. So it looks like some failed attempt of capturing a screenshot. But why would it happen when the only thing I was trying to do is to press a button (any button) on the app right after its startup?
If anybody knows an answer to this question, please let me know!
Friday, 4 November 2011
Android Annoyances: RadioGroup
I have a RadioGroup
with two RadioButton
s in it. In the code, I want to know which of the buttons was selected. Here is the way to find out that information using Android API:
int index = group.indexOfChild(group.findViewById(group.getCheckedRadioButtonId()));
Pure elegance…
Tuesday, 25 October 2011
Android Annoyances: Text Length
It is incomprehensible to me why there are properties that you can set in an XML attribute but not in code. Here is one quite obvious example.
I have an EditText
control for text input and I want to limit the length of input to just two characters. No problem, I can do that easily in XML layout:
android:maxLength="2"
The problem is: in some circumstances I want to limit input to only 1 character, and can only do that in code. Surely enough, if there is such an XML attribute, there should be a setMaxLength()
method, right? Wrong. The guys who designed Android API just didn’t think it might be needed.
Rubbish.
Saturday, 22 October 2011
Quartz 2D Article
Part 1. Preparing the environment, drawing the grid lines.
Part 2. Drawing a bar graph, with gradient fill.
Part 3. Drawing a line graph, with gradient fill.
Part 4. Making graphs interactive.
Part 5. (to be published soon) Drawing text on graphs (labels etc).