[Java for Android App Development] frangulyan.dev:
...CONTINUATION of the original post.

We also set dither to 'true'. It is always a good idea to set dither to true when you are drawing gradients. For more information you can go here where this famous Android Guy is showing some examples of dithering.

Lets go to the createGradient() function:

...
private Float mXAngle = null;
private Float mYAngle = null;
...

private void createGradient() {
if (mBounds.height() == 0) {
return;
}
int startColor = mBorderColor;
float x0 = mBounds.left;
float y0 = mBounds.bottom;
float x1 = mBounds.right;
float y1 = mBounds.top;
if (mXAngle != null && mYAngle != null) {
if (mXAngle == 0 && mYAngle == 0) {
startColor = mGradientEndColor;
} else {
float h = mBounds.height();
float w = mBounds.width();
float radius = (float) Math.sqrt(h * h + w * w) / 2.0f;
float norm = radius / (float) Math.sqrt(mXAngle * mXAngle + mYAngle * mYAngle);
x0 = mBounds.centerX() + mXAngle * norm;
y0 = mBounds.centerY() + mYAngle * norm;
x1 = mBounds.centerX() - mXAngle * norm;
y1 = mBounds.centerY() - mYAngle * norm;
}
}
mBorderGradient = new LinearGradient(x0, y0, x1, y1,
startColor, mGradientEndColor, Shader.TileMode.CLAMP);
}

The variables mXAngle and mYAngle are set from outside using gyroscope data. We will come to that later. Now think of them as of a vector - they are showing a direction. We are using this direction as a direction of our gradient.

In order to fully define a linear gradient we need 2 (x,y) points on the plane - a start point and end point as shown in the picture below.

[IMG="linear gradient"]https://googledrive.com/host/0Bw-1MvcHCIh0ck5wVzlUbmFNT0k/android_gradient.png[/IMG]

Note that these points should be in the coordinate system of the view, that is why are using mBounds variable. The shader mode is set to CLAMP so that outside these bounds the color of the corresponding point is used.

Then comes some math. We set the default "view angle" to the direction from left-bottom to top-right. After that, if there is a direction set, we calculate these 2 gradient points as an intersection of the rectangle's outer circle and the direction line passing through the center of the rectangle. If the direction vector is zero then both gradient colors are set to transparent and no border is drawn (view angle "from top"). First we calculate the radius of rectangle's outer circle - the distance from the center of the rectangle to its corners. Then we calculate a normalization factor and finally put the view direction vector in the center of the rectangle and multiply by normalization factor in order to put its endpoint on the outer circle. The gradient is ready.

In the picture below you can see the result of drawing a round rectangle border with a linear gradient shader:

[IMG="gradient border"]https://googledrive.com/host/0Bw-1MvcHCIh0ck5wVzlUbmFNT0k/android_rect_3d.png[/IMG]

The final step is to set the "view angle" based on gyroscope values:

public void onGyroChanged(float xAngle, float yAngle) {
mXAngle = xAngle;
mYAngle = yAngle;
invalidate();
}

This function sets the new angle values and calls invalidate() to redraw self.

The gyroscope processing is out of the scope of this post, so I will just paste the code here. It is mostly copied from Android documentation:

public class GyroHelper implements SensorEventListener {

private static final float NS2S = 1.0f / 1000000000.0f;
private Display mDisplay;
private boolean mStarted = false;
private SensorManager mManager;
private long mLastTime = 0;
private float mAngleX = 0.0f;
private float mAngleY = 0.0f;

public GyroHelper(Context c) {
mManager = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE);
WindowManager wm = (WindowManager) c.getSystemService(Context.WINDOW_SERVICE);
mDisplay = wm.getDefaultDisplay();
}

public static boolean canBeStarted(Context c) {
SensorManager manager = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE);
return manager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
}

public void start() {
mStarted = false;
Sensor sensor = mManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
if (sensor == null) {
return;
}
mStarted = true;
reset();
mManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI);
}

public void stop() {
mStarted = false;
reset();
mManager.unregisterListener(this);
}

public boolean isStarted() {
return mStarted;
}

public float getXAngle() {
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0: return -mAngleY;
case Surface.ROTATION_90: return -mAngleX;
case Surface.ROTATION_180: return mAngleY;
case Surface.ROTATION_270: return mAngleX;
}
return mAngleX;
}

public float getYAngle() {
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0: return -mAngleX;
case Surface.ROTATION_90: return mAngleY;
case Surface.ROTATION_180: return mAngleX;
case Surface.ROTATION_270: return -mAngleY;
}
return mAngleY;
}

@Override
public void onSensorChanged(SensorEvent event) {
if (mLastTime != 0) {
final float dT = (event.timestamp - mLastTime) * NS2S;
mAngleX += event.values[0] * dT;
mAngleY += event.values[1] * dT;
}
mLastTime = event.timestamp;
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}

private void reset() {
mLastTime = 0;
mAngleX = 0.0f;
mAngleY = 0.0f;
}

}

Somebody needs to start this GyroHelper and periodically call BorderItemView.onGyroChanged(GyroHelper.getXAngle(), GyroHelper.getYHelper()). That can be done right when onSensorChanged() is fired, but it is not a good idea since that can be too often, you might want to have your own timer controlling your frame rate, sensor updates can come 200 times in a second.

Finally, to see this code in action, you can have a look at the app itself, its free:

Google Play:

[IMG="Arithmetic Puzzles on Google Play"]http://developer.android.com/images/brand/en_generic_rgb_wo_45.png[/IMG]

Direct link:

Download

It would be nice to get some comments and suggestions, let me know your thoughts!