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

Now lets see how is the mShineGradient created. This is done every time we got a new data from gyroscope:

...
// all the magic numbers here and in below function are results of experiments
private static final float MAX_ANGLE = (float)(Math.PI / 2.0);
private final float mShadowMaxShift = 5.0f * getResources().getDisplayMetrics().density; // 5dp
...

public void gyroChanged(float xAngle, float yAngle) {
// 1. shadows
float loweredMax = MAX_ANGLE / 4;
mShadowShiftX = (xAngle / loweredMax) * mShadowMaxShift;
mShadowShiftY = (yAngle / loweredMax) * mShadowMaxShift;

// put in [-mShadowMaxShift, mShadowMaxShift] range
if (mShadowShiftX > mShadowMaxShift) mShadowShiftX = mShadowMaxShift;
if (mShadowShiftX < -mShadowMaxShift) mShadowShiftX = -mShadowMaxShift;
if (mShadowShiftY > mShadowMaxShift) mShadowShiftY = mShadowMaxShift;
if (mShadowShiftY < -mShadowMaxShift) mShadowShiftY = -mShadowMaxShift;

// 2. shine
float angleX = xAngle / MAX_ANGLE;
float angleY = yAngle / MAX_ANGLE;

// put in [-1, 1] range
if (angleX > 1.0f) angleX = 1.0f;
if (angleX < -1.0f) angleX = -1.0f;
if (angleY > 1.0f) angleY = 1.0f;
if (angleY < -1.0f) angleY = -1.0f;

createShineGradient(angleX, angleY);

// redraw
invalidate();
}

The numbers and formulas are quite experimental, so you can play around to find the best numbers for your case. The meaning and usage of gyroChanged() function is explained in my previous guide.

The basic idea behind is to get the shine position based on device's rotation in X and Y direction. I convert the rotation into a range from -1 to 1 using some max angle that I defined. If both X and Y angles are -1 then the shine line is in the lower left corner of the text, if both are 1 then in upper right corner, otherwise somewhere in between.

Here is the createShineGradient() function:

...
private static final float SHINE_WIDTH = 0.07f;
private static final float SHINE_BLUR_WIDTH = 0.05f;
...

private void createShineGradient(float relativeX, float relativeY) {
if ((mBounds == null) || (mBounds.width() == 0) || (mBounds.height() == 0)) {
mShineGradient = null;
return;
}

// we want to scale the angles' range and take inner part of
// length 1 this will speed up the shine without sudden stops
final float SPEED_FACTOR = 4.0f;
relativeX *= SPEED_FACTOR;
relativeY *= SPEED_FACTOR;

float boxSize = mBounds.height() * 1.2f; // make the text box a bit bigger
float left = mBounds.centerX() - boxSize / 2.0f;
float top = mBounds.top;

// project the (relativeX, relativeY) point to the diagonal
float relative = (relativeX + relativeY) / 2.0f;
// shift by 0.5 to get a point from (0, 1) range
relative += 0.5f;

int[] colors = {mShineNoColor, mShineNoColor, mShineColor, mShineColor, mShineNoColor, mShineNoColor};
float[] positions = {0.0f, clamp(relative - SHINE_WIDTH - SHINE_BLUR_WIDTH),
clamp(relative - SHINE_WIDTH), clamp(relative + SHINE_WIDTH),
clamp(relative + SHINE_WIDTH + SHINE_BLUR_WIDTH), 1.0f};

mShineGradient = new LinearGradient(left, top + boxSize, left + boxSize, top,
colors, positions, Shader.TileMode.CLAMP);
}

private float clamp(float value) {
if (value < 0.0f) {
return 0;
}
if (value > 1.0f) {
return 1.0f;
}
return value;
}

Again, there are a lot of experimental stuff, you might want to play with it to come to a good solution. The LinearGradient shader is explained in my previous guide. However, here we use more colors so that we can have a white stripe in the middle with small color change gradients on borders. The picture below explains everything:

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

The idea is to project the relative angle of the device rotation in X and Y direction to a single point on the View's diagonal through which the shine will pass. This projection should result in continuous and more or less natural movement of the shine line during device rotation, the formula I used is a result of my tries and errors.

When a new gradient is created invalidate() is called and the view redraws itself.

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!