Leonardo Garcia Fischer

LinkedIn Facebook Twitter Feeds

Hosting Android Widgets – My AppWidgetHost Tutorial

30Jan'12

Written by Leonardo Fischer

Hi,

No, this isn’t another tutorial on how to create Android Widgets. For this, I recommend you the Android SDK or Google. This post is on how to create a simple app that lets the user add and remove widgets, like the Android Home Screen does.

I decided to write this one because I couldn’t find anything on the web saying how to do this. I found how to create this example looking at the Android Home Screen Source Code (AHSSC). So, if you already did this, you may find some variable names similar. You can use this as trails to look yourself on the AHSSC ツ

Initialization

You start by creating two objects. The first is an AppWidgetManager, which will give you the data you need about installed widgets. The second one is an AppWidgetHost, which will keep in memory your widget instances. Latter, your app will handle only the view that will draw the widget:

mAppWidgetManager = AppWidgetManager.getInstance(this);
mAppWidgetHost = new AppWidgetHost(this, R.id.APPWIDGET_HOST_ID);

Selecting the Widget

You start by asking to the AppWidgetHost to allocate resources for a widget instance. It will return an ID for that. Then, you need to start an activity to let the user select which widget he wants to add to your app. You need to give this ID to the activity.

void selectWidget() {
    int appWidgetId = this.mAppWidgetHost.allocateAppWidgetId();
    Intent pickIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_PICK);
    pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    addEmptyData(pickIntent);
    startActivityForResult(pickIntent, R.id.REQUEST_PICK_APPWIDGET);
}
void addEmptyData(Intent pickIntent) {
    ArrayList customInfo = new ArrayList();
    pickIntent.putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_INFO, customInfo);
    ArrayList customExtras = new ArrayList();
    pickIntent.putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_EXTRAS, customExtras);
};

Unfortunately, any kind of software has bugs, and here is one of the Android SDK. The Widget API supports that you merge custom widgets of your application with the installed ones. But if you don’t add anything, the Activity that shows the list of widgets to the user crashes with a NullPointerException. The addEmptyData() method above adds some dummy data to avoid this bug. More on this bug here. If you want to add a custom widget, start looking at this point of the AHSSC.

Configuring the Widget

If the user successfully selects a widget from the list (he didn’t pressed “back”), it will return an OK to you as an activity result. The data for this result contains the widget ID. Use it to retrieve the AppWidgetProviderInfo to check if the widget requires any configuration (some widgets does need). If it requires, you need to launch the activity to configure the widget. If not, jump to the next step.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == RESULT_OK ) {
        if (requestCode == REQUEST_PICK_APPWIDGET) {
            configureWidget(data);
        }
        else if (requestCode == REQUEST_CREATE_APPWIDGET) {
            createWidget(data);
        }
    }
    else if (resultCode == RESULT_CANCELED && data != null) {
        int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
        if (appWidgetId != -1) {
            mAppWidgetHost.deleteAppWidgetId(appWidgetId);
        }
    }
}

private void configureWidget(Intent data) {
    Bundle extras = data.getExtras();
    int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
    if (appWidgetInfo.configure != null) {
        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
        intent.setComponent(appWidgetInfo.configure);
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        startActivityForResult(intent, REQUEST_CREATE_APPWIDGET);
    } else {
        createWidget(data);
    }
}

Creating and Adding it to Your Views

Now is time to create the widget itself. You will use the Widget ID and the AppWidgetProviderInfo to ask to the AppWidgetHost “could you please create a view of this widget for me?“. It will return an AppWidgetHostView which is a derived class from View. This one you can handle as any other view from the Framework. But don’t forget to set the Widget ID and Widget Info on the view (I don’t know why the AppWidgetHost didn’t when creating the view).

public void createWidget(Intent data) {
    Bundle extras = data.getExtras();
    int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
    AppWidgetHostView hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
    hostView.setAppWidget(appWidgetId, appWidgetInfo);
    layout.addView(hostView);
}

Updating

The widget is now working, but is not being updated by your app. If the widget is a clock, it will be stuck at the time you added it. To register the widget to receive the events it needs, call startListening() on the AppWidgetHost. To avoid wasting battery with unnecessary updates while your app is not visible, call it during the onStart() method of your activity, and call stopListening() during the onStop() method.

@Override
protected void onStart() {
    super.onStart();
    mAppWidgetHost.startListening();
}
@Override
protected void onStop() {
    super.onStop();
    mAppWidgetHost.stopListening();
}

Releasing the Widget

The widget should be working now. But if you want to remove the widget, you need to ask to the AppWidgetHost to release it. If you do not release it, you’ll get a memory leak (your app will consume unnecessary memory). Finally, remove it from your LayoutView.

public void removeWidget(AppWidgetHostView hostView) {
    mAppWidgetHost.deleteAppWidgetId(hostView.getAppWidgetId());
    layout.removeView(hostView);
}

Note that the widget ID is also deleted during the onActivityResult() method if the user gave up selecting the widget.

I hope this can help you develop widget based apps. You can download the full source code for this post here or on GitHub. There is also an APK to install on your phone (just make sure you can install it).

Tags:

31 comments

NinePatches, backgrounds, paddings, relayouts and some headache

10Nov'11

Written by Leonardo Fischer

Hi,

A few days ago I got stuck into a so called “bug”. A developed a small NinePatch to use in an Android app. I wanted it to highlight a ViewGroup object, and then remove the background later. But the first time I set the NinePatch, the view appears to move a little bit. Weird!

Let’s start from the beginning: what is a NinePatch?

It is a special image file. In the Android SDK, it is a file with the extension .9.png that you can open in any image editor. What makes it special is a border around the image that has a special meaning for the Android system. If you ask to draw this image with a different size than its width*height dimensions, the image will stretch only in some pre-defined areas and the others will keep their original size. Why you would use this?? A practical way to explain this is to show you a NinePatch in use:

There is a NinePatch on the left (with a lot of zoom, and a yellow line to the actual NinePatch dimensions). On the right, two buttons with different dimensions. You got it? The NinePatch is very stretched on the big button, but still look very good! There is a lot of material explaining how to get this effect on the web. I recommend you the official Android SDK for this (which also is the source of the image with the buttons, modified by me). The Android SDK also have a very simple tool that let’s you generate these NinePatches.

The Symptom

So far, so good, the NinePatch works pretty well. Until you put it behind a view as its background. What happens? Let’s see.

After I set the NinePatch as the background of the FrameLayout root_layout, the text is moved some pixels down (I drew a blue line so you can see that it moved exactly 4 pixels). Not too much, but it should move 0 pixels, no more, no less!

The Research for the Cure of the Headache

Well, actually the “bug” is not a “bug”, but a feature! After researching the Android source a little bit, I understood that the optional padding that you should set in the NinePatch also gets into account when you set the ViewGroup’s background. Let’s look at the source of the method View.setBackgroundDrawable(Drawable d).

public void setBackgroundDrawable(Drawable d) {
    //some other code
    if (d != null) {
        Rect padding = sThreadLocal.get();
        //more intermediate code
        if (d.getPadding(padding)) {
            setPadding(padding.left, padding.top, 
                padding.right, padding.bottom);
        }
        //more code
    }
    //and the finishing code
}

The Medicine

As you can see, the view literally gets the padding that you set into the NinePatch and sets onto the view, changing its dimension. That is why you set the background and the view changes its position. If you do not set the optional padding, the padding of the NinePatch will be computed from the stretchable area, and will mess with your beautiful layout.

Optionally, you can get the padding before set the background, set your custom background, and then set the old padding. Example:

//backup the old padding
Rect padding = new Rect();
padding.left = rootLayout.getLeft();
padding.top = rootLayout.getTop();
padding.right = rootLayout.getRight();
padding.bottom = rootLayout.getPaddingBottom();
//set the new background
rootLayout.setBackgroundResource(R.drawable.nine_patch);
//restore the old padding
rootLayout.setPadding(padding.left, padding.top, 
    padding.right, padding.bottom);

This one is not as good as just set the padding on the NinePatch, as you need to run some extra code every time you set a background onto a view. But this hack may let you do something that I did not though of yet ツ

The Side-Effects

Finally, I want to show you a side effect of using a NinePatch. Let’s go back to the Android source code:

public void setBackgroundDrawable(Drawable d) {
    boolean requestLayout = false;
    //some initial code
    if (d != null) {
        // some other code, including the one presented above
        if (mBGDrawable == null || 
                mBGDrawable.getMinimumHeight() != d.getMinimumHeight() ||
                mBGDrawable.getMinimumWidth() != d.getMinimumWidth()) {
            requestLayout = true;
        }
        // more code
    }
    // pre-requestLayout code
    if (requestLayout) {
        requestLayout();
    }
    //finishing code
}

As you can see, if the previous background has different minimum width or height from the new one, the method will force a requestLayout() call. This is ok if you set the background during the application initialization. But if you start to swap your view’s background, then you need to take care of these properties too. If not, your application may suffer from “hiccups” from the re-layout of your views.

Finishing, this is the source code that I developed for this post. It contains the “Hello World” example you saw above. If you click on the text, the background is added, so you can see for yourself this Android feature.

Tags:

No comments yet

Android SDK: Using the Scroller object to create a page flip interaction

23Oct'11

Written by Leonardo Fischer

This weekend I decided to play with the Android SDK. I decided to implement an app that uses the basic sweep gesture: you can touch the buttons, but you can drag to change the current page smoothly, or use a gesture to flip between views. The one from the Home screen of most Androids phones and iPhones. There are several solutions on the net, but I thought that it will be great if I developed one on my own. And it was ツ

So, this is my solution, the PageFlipper class. I extended the ViewGroup, so each child you add to my class will be a page on the final view. I use the onInterceptTouchEvent method to check if the user wants to drag/flip the screen or just want to click on a button on it. If I understand the user’s intent to change the page, the method returns true, and the motion of the screen is divided between the onTouchEvent and the computeScroll methods.

My solution uses a very simple State Machine. Take a look at it. I will explain the code based on it.

1) Firstly, the user touches the screen. In the onInterceptTouchEvent, I capture the initial coordinates of the touch, and changes to the state ST_WAITING. The VelocityTracker is initialized here to compute the velocity that the user moves his finger on the screen. Note that I return false from here because I want to watch the MotionEvent, but I still want to let the target child receive the event.

if (action == MotionEvent.ACTION_DOWN && getState() == ST_IDLE) {
    if (mVTracker != null) {
        mVTracker.recycle();
    }
    mVTracker = VelocityTracker.obtain();
    mVTracker.addMovement(event);
    setState(ST_WATCHING);
    mLastX = mFirstX = (int) event.getX();
    mFirstY = (int) event.getY();
    return false;
}

2) The user leaves his finger from the screen. Just go back to the ST_IDLE state, and releases the VelocityTracker object.

if (action == MotionEvent.ACTION_UP || 
        action == MotionEvent.ACTION_CANCEL) {
    setState(ST_IDLE);
    mVTracker.recycle();
    mVTracker = null;
}
return false;

3) Now the things are getting interesting. The user’s finger moved slowly across the screen. What we do here is check if he moved long enough on the horizontal axis to assume that the screen should be moved. If it is true, changes to the ST_DRAGGING state. By returning true here, the current and the next MotionEvents will be delivered direct to the onTouchEvent of my class. The ST_IGNORING is just to avoid interaction with the PageFlipper while the user is interacting with, let’s say, a list in one of the pages.

if (action == MotionEvent.ACTION_MOVE && getState() == ST_WATCHING) {
    int deltaX = Math.abs(mFirstX - (int) event.getX());
    int deltaY = Math.abs(mFirstY - (int) event.getY());
    if (deltaX > mTouchSlop && deltaY < mTouchSlop) {
        setState(ST_DRAGGING);
        return true;
    }
    if (deltaX < mTouchSlop && deltaY > mTouchSlop) {
        setState(ST_IGNORING);
        return false;
    }
}

4) While the user is moving his finger on the screen, I scroll the view. Doing this way the user can actually drag the views on the screen.

if (getState() == ST_DRAGGING && action == MotionEvent.ACTION_MOVE) {
    int deltaX = mLastX - (int) event.getX();
    scrollBy(deltaX, 0);
    mLastX = (int) event.getX();
}

5 and 6) When the user stops touching the screen, the state changes to ST_ANIMATING. Here are several things to do. First, I use the VelocityTracker to compute the speed that the user moved his finger. If it is greater than a minimum speed, the animation will scroll the view to the next child on the left or right. If the user moved his finger slowly, but for more than 50% of the screen, it is moved to the left or right too. If not, then move the view to the correct position back again.

if (getState() == ST_DRAGGING && (action == MotionEvent.ACTION_UP
        || action == MotionEvent.ACTION_CANCEL)) {
    setState(ST_ANIMATING);
    mVTracker.computeCurrentVelocity(1000);
    float velocity = mVTracker.getXVelocity();
    final int width = getWidth();
    final int delta = mLastX - mFirstX;
    final boolean fling = Math.abs(velocity) > mFlingSlop;
    final boolean moveToNextScreen = Math.abs(delta) > (width / 2);
    if (fling || moveToNextScreen) {
        int motion = (delta > 0 ? -1 : 1) * (width - Math.abs(delta));
        mScroller.startScroll(getScrollX(), getScrollY(), motion, 0);
    } else {
        mScroller.startScroll(getScrollX(), getScrollY(), delta,  0);
    }
    invalidate();
    mLastX = mFirstX = mFirstY = -1;
    mVTracker.recycle();
    mVTracker = null;
}

Note that I call the invalidate() method on the end. This will force the view to redraw itself. But before that, the SDK will call the computeScroll() method, which is the key to animate the scroll of our view. Note that the methods scrollTo() and scrollBy() just move the view at once to the specified position. So, the Scroller object will help us to move just a little bit at a time, to give the feeling of an animation.

7 and 8) Finishing the main code, our computeScroll() will be called after the call to invalidate() that we did before. The computeScrollOffset() method will return true until the scroll has been completed. So we move a little bit again the view, and call invalidate() again. When the scroller is finished, go back to the state ST_IDLE.

public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        invalidate();
    } else {
        if (getState() == ST_ANIMATING && mScroller.isFinished())
            setState(ST_IDLE);
        super.computeScroll();
    }
}

There are some points that I didn’t solved in this code. One is that the view gets lost if you try to sweep to the left of the first view or the right of the last view. Also, if you add a child view that doesn’t receive touchEvents (such as a TextView), the MotionEvents are not working as I would expect. You need to call setClickable(true) in these cases.

Please, download the full source code of the PageFlipper class and an running example. I hope that you can use it for your projects. And, if you use it or find a bug, please leave a comment bellow ツ

Tags:

3 comments

css.php