Friday, April 20, 2012

Dialog box by PopupWindow

While screen size is increasing dialog boxes become essential part of most applications. Once in early February 2011 first Motorolla Xoom appeared on the shelves this problem became actual for most android developers. Of course, simple dialogs (to confirm some stuff deletion or agree with license agreement) have been used previously to smartphone applications. But the key points of the user interface were spread to the individual pages (Activities). Pages which looked good on small screens of smartphones became very ugly (because a lot of wasted space) on large screens of tablets.
As a quick solution for our problem Android SDK 11 offers us set of holographic themes (more commonly known as themes) among which there are Theme_Holo_Dialog and Theme_Holo_DialogWhenLarge (as well as few derived from them themes stylized for light colors and w.o. action bar). We won't deepen in these topics - I think every one faced with these stuff (besides, internal implementation of these themes deserves own investigation). Just note that using these themes we got quite nice dialogs stylized for holographic themes.
This dialog boxes were quite limited in styling - to perform even small changes in dialog positioning or appearance sometimes developer had to engaged in a tedious search for correct property in appropriate style. Obviously, we can't have 2 activity in foreground, so the question about the 2 open dialogs at the same time no longer relevant. Also do not forget the 'heaviness' of Activities. But support of activity may be a partial compensation for previous inconvenience.

Let's look at alternative approach (in my opinion unjustly deprived of attention) - PopupWindow. Been present from very first sdk, PopupWindow wasn't very popular (at least, I could not find more information than simple example how to create simple popup window). In my opinion, this is very configurable container, which gives us much more advantages than activity-dialog, so let's look on it closer.
First of all, there are 2 views to hold content:
   private View mContentView;  
   private View mPopupView;  

Depending on whether was mBackground provided or not (using PopupWindow#setBackgroundDrawable) mPopupView will be assigned whole mContentView or even composite PopupViewContainer (FrameLayout actually):
   private void preparePopup(WindowManager.LayoutParams p) {  
     ...  
     if (mBackground != null) {  
       final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();  
       int height = ViewGroup.LayoutParams.MATCH_PARENT;  
       if (layoutParams != null &&  
           layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {  
         height = ViewGroup.LayoutParams.WRAP_CONTENT;  
       }  
       // when a background is available, we embed the content view  
       // within another view that owns the background drawable  
       PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);  
       PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(  
           ViewGroup.LayoutParams.MATCH_PARENT, height  
       );  
       popupViewContainer.setBackgroundDrawable(mBackground);  
       popupViewContainer.addView(mContentView, listParams);  
       mPopupView = popupViewContainer;  
     } else {  
       mPopupView = mContentView;  
     }  
     ...  
   }  
PopupViewContainer is a key point to answer on frequently asked question 'why do we need set setBackgroundDrawable to close our popup window when outside area is touched?' - PopupViewContainer is listening to the touch event and dismisses popup window:
     @Override  
     public boolean onTouchEvent(MotionEvent event) {  
       final int x = (int) event.getX();  
       final int y = (int) event.getY();  
       if ((event.getAction() == MotionEvent.ACTION_DOWN)  
           && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {  
         dismiss();  
         return true;  
       } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {  
         dismiss();  
         return true;  
       } else {  
         return super.onTouchEvent(event);  
       }  
     }  
Content of PopupWindow is added to Activity by WindowManager (ViewManager#addView method is used). We are not limited in number of PopupWindows that can be added to page. But since ViewManager#addView creates new 'layer' for every PopupWindow they will be closed (in case of touch at outside area) in reverse order about how they were added.
From the fact that content of PopupWindow is View follows that we can apply animations to dialog box (using, for example, View#startAnimation). It may be useful when we want to show/hide dialog box with move effect or something like this (this often happens when we copy the behavior of ios apps :))
Finally, very often problem when you tried to create to show PopupWindow from activity lifecycle method:
 E/AndroidRuntime( 524): java.lang.RuntimeException: Unable to resume activity {packagename/packagename.ActivityName}: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?  

For those who haven't faced this problem here is solution: add your popup window showing to the message queue (using post method) to be run after all the lifecycle methods finish:
     viewRoot.post(new Runnable() {  
       public void run() {  
         popupWindow.showAtLocation(viewRoot, Gravity.TOP | Gravity.LEFT, 0, 0);  
       }  
     });  
Looks like this is essential requirement. I couldn't find explanation why this happens, looks like we have to wait until everything in Activity is set up.
Summing up, I want to say, that PopupWindow is a great choice when you need lightweight (and configurable) mechanism to communicate with user.

No comments:

Post a Comment