Permalink

12

Android custom view: ChartView

While writing Quotes, I was very unsatisfied with the options available for Android chart views. A lot of them are very feature rich, but cumbersome and broken. When I say broken, I’m referring to the guidelines set out by Google for custom views:

  • Conform to Android standards
  • Provide custom styleable attributes that work with Android XML layouts
  • Send accessibility events
  • Be compatible with multiple Android platforms.

Instead of settling for what was out there, I wrote a custom chart view to do what I wanted while conforming to Google’s standards. It’s not as full featured as other chart views out there, but it has all the features I needed at the time I wrote it. It’s meant to be customizable and extended, and I think the result was quite nice.

ChartView

I just pushed the code for ChartView to my AndroidUtils library on Github, so go check it out!

https://github.com/pardom/AndroidUtils/tree/master/src/com/michaelpardo/android/widget/chartview

Update: A few have asked for an example of how to use ChartView, so I’ve added one to the wiki.

https://github.com/pardom/AndroidUtils/wiki/Using-ChartView

Permalink

4

ActiveAndroid gets update, goes open source!

ActiveAndroid just has undergone quite a few changes and a major refactor. Speed is up, and support has been added for database attachment. ActiveAndroid’s commercial status has supported its growth in the past couple years, but I’ve decided to be truer to Android’s open source nature, and publish it on Github. I feel that the move to open source will only make ActiveAndroid better for everyone.

Thanks to everyone who has supported ActiveAndroid in the past, and go check out the source over at Github!

http://github.com/pardom/ActiveAndroid
http://www.activeandroid.com

Permalink

2

Recording audio streams

My most recent app RingDimmer relies heavily on the use of the device microphone. This is not a trivial operation as there are many devices with many specifications, many of which don’t conform to the minimum requirements set out in the Android docs.

As such, I’ve decided to lend my fellow developers a hand and release the class I created to handle audio recording. I’m sure the class will need to improve over time, as new device configurations appear. So far it has been pretty reliable, but I welcome your comments and critiques.

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder.AudioSource;
import android.os.Process;

public class AudioMeter extends Thread {
    /////////////////////////////////////////////////////////////////
    // PUBLIC CONSTANTS

    // Convenience constants
    public static final int AMP_SILENCE = 0;
    public static final int AMP_NORMAL_BREATHING = 10;
    public static final int AMP_MOSQUITO = 20;
    public static final int AMP_WHISPER = 30;
    public static final int AMP_STREAM = 40;
    public static final int AMP_QUIET_OFFICE = 50;
    public static final int AMP_NORMAL_CONVERSATION = 60;
    public static final int AMP_HAIR_DRYER = 70;
    public static final int AMP_GARBAGE_DISPOSAL = 80;

    /////////////////////////////////////////////////////////////////
    // PRIVATE CONSTANTS

    private static final float MAX_REPORTABLE_AMP = 32767f;
    private static final float MAX_REPORTABLE_DB = 90.3087f;

    /////////////////////////////////////////////////////////////////
    // PRIVATE MEMBERS

    private AudioRecord mAudioRecord;
    private int mSampleRate;
    private short mAudioFormat;
    private short mChannelConfig;

    private short[] mBuffer;
    private int mBufferSize = AudioRecord.ERROR_BAD_VALUE;

    private int mLocks = 0;

    /////////////////////////////////////////////////////////////////
    // CONSTRUCTOR

    private AudioMeter() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
        createAudioRecord();
    }

    /////////////////////////////////////////////////////////////////
    // PUBLIC METHODS

    public static AudioMeter getInstance() {
        return InstanceHolder.INSTANCE;
    }

    public float getAmplitude() {
        return (float) (MAX_REPORTABLE_DB + (20 * Math.log10(getRawAmplitude() / MAX_REPORTABLE_AMP)));
    }

    public synchronized void startRecording() {
        if (mAudioRecord == null || mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            throw new IllegalStateException("startRecording() called on an uninitialized AudioRecord.");
        }

        if (mLocks == 0) {
            mAudioRecord.startRecording();
        }

        mLocks++;
    }

    public synchronized void stopRecording() {
        mLocks--;

        if (mLocks == 0) {
            if (mAudioRecord != null) {
                mAudioRecord.stop();
                mAudioRecord.release();
                mAudioRecord = null;
            }
        }
    }

    /////////////////////////////////////////////////////////////////
    // PRIVATE METHODS

    private void createAudioRecord() {
        if (mSampleRate > 0 && mAudioFormat > 0 && mChannelConfig > 0) {
            mAudioRecord = new AudioRecord(AudioSource.MIC, mSampleRate, mChannelConfig, mAudioFormat, mBufferSize);

            return;
        }

        // Find best/compatible AudioRecord
        for (int sampleRate : new int[] { 8000, 11025, 16000, 22050, 32000, 44100, 47250, 48000 }) {
            for (short audioFormat : new short[] { AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_8BIT }) {
                for (short channelConfig : new short[] { AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_STEREO,
                        AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.CHANNEL_CONFIGURATION_STEREO }) {

                    // Try to initialize
                    try {
                        mBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

                        if (mBufferSize < 0) {
                            continue;
                        }

                        mBuffer = new short[mBufferSize];
                        mAudioRecord = new AudioRecord(AudioSource.MIC, sampleRate, channelConfig, audioFormat,
                                mBufferSize);

                        if (mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
                            mSampleRate = sampleRate;
                            mAudioFormat = audioFormat;
                            mChannelConfig = channelConfig;

                            return;
                        }

                        mAudioRecord.release();
                        mAudioRecord = null;
                    }
                    catch (Exception e) {
                        // Do nothing
                    }
                }
            }
        }
    }

    private int getRawAmplitude() {
        if (mAudioRecord == null) {
            createAudioRecord();
        }

        final int bufferReadSize = mAudioRecord.read(mBuffer, 0, mBufferSize);

        if (bufferReadSize < 0) {
            return 0;
        }

        int sum = 0;
        for (int i = 0; i < bufferReadSize; i++) {
            sum += Math.abs(mBuffer[i]);
        }

        if (bufferReadSize > 0) {
            return sum / bufferReadSize;
        }

        return 0;
    }

    /////////////////////////////////////////////////////////////////
    // PRIVATE CLASSES

    private static class InstanceHolder {
        private static final AudioMeter INSTANCE = new AudioMeter();
    }
}

Basically what this class does is construct a valid AudioRecord Object, wrap AudioRecord methods, and provide conversion to decibels. It also caches the AudioRecord configuration and prevents multiple instances of the recorder.

Feel free to use as you see fit, and be sure to check out RingDimmer!

Permalink

0

Falling back on old SDK methods without reflection

As Android progresses as a platform, there will inevitably be methods added to the SDK. You’ll want to add some of these great new features while still supporting the old SDKs.

We can conditionally use these methods by getting the device’s SDK version and using the appropriate method, but this won’t actually work because although it may compile, the older device will throw an exception when it loads the class that uses that unknown method.

One way to fix this is via reflection, which works great. Reflection, however, can be resource intensive, and hard to maintain. You’ll have to declare the method as a string and pass in a method signature… gross.

Another way to do this builds on the first approach. Since a method isn’t loaded until the class it’s in is loaded, we can hide the method in another class. Here’s how it works:

public class MyActivity extends ListActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        // Check to see if this version of Android supports
        // ListView smooth scrolling
        if(Integer.parseInt(Build.VERSION.SDK) >= 8) {
            // Smooth scroll to position
            SmoothScrollMethodHolder.smoothScrollToPosition(getListView(), 5);
        }
        else {
            // Set postion
            getListView().setSelection(5);
        }
    }
   
    private class SmoothScrollMethodHolder {
        // Put method in a static method of a private class. This class
        // won't be loaded until you call this statically method.
        public static void smoothScrollToPosition(ListView listView, int position) {
            listView.smoothScrollToPosition(position);
        }
    }
}

This code is easy to read, easy to maintain, and comes with all the benefits that reflection strips away, like code completion. Of course, you don’t want to litter your code with the above, so you’ll probably want to put it into a utility class.

public static class CompatibilityUtils {
    private static final int SDK_VERSION = Integer.parseInt(Build.VERSION.SDK);

    public static void smoothScrollToPosition(ListView listView, int position) {
        if(SDK_VERSION >= 8) {
            API9.smoothScrollToPosition(listView, position);
        }
        else {
            listView.setPosition(position);
        }
    }
   
    private static class API6 {
        ...
    }

    private static class API8 {
        public static void smoothScrollToPosition(ListView listView, int position) {
            listView.smoothScrollToPosition(position);
        }
    }
   
    private static class API9 {
        ...
    }
   
    private static class API10 {
        ...
    }
   
    private static class API14 {
        ...
    }
}

You can add all your compatibility methods to this class. The original code is now just:

CompatibilityUtils.smoothScrollToPosition(getListView(), 5);
Permalink

2

Inset TextView shadows

If you’re used to using Apple products, you’re probably familiar with the heavy use of inset shadows. It adds a bit of depth to the UI and can really make the screen look beautiful. Notice the white drop shadow in the title of the window.

Finder

I tend to do the same thing a lot for my Android apps. It’s incredibly simple to do. Here’s an example of a TextView with the same inset shadow.

<!-- Semi-opaque white inset shadow beneath the text -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    ...
    android:shadowColor="#88FFFFFF"
    android:shadowRadius="0.1"
    android:shadowDx="0"
    android:shadowDy="1" />
       
<!-- Semi-opaque black inset shadow above the text -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    ...
    android:shadowColor="#88000000"
    android:shadowRadius="0.1"
    android:shadowDx="0"
    android:shadowDy="-1" />

And here’s how it looks:

Inset shadows

It should look great on all screen sizes. Don’t use the above code strictly, however. Mess around with the color values and shadowRadius value to get the exact effect you’re looking for.