I want to share some codes of a small effect that I implemented in my Android app called Arithmetic Puzzles. This is also a chance for me to listen to other people and make improvements. At the end of this post there is a link to the app so that you can see the code in action.
It is a pseudo-3D effect of the playing board items which are looking like rotating slightly based on your viewing angle when you move the device. The effect is not something of very visible but this small non-disturbing animations usually please the user and make the app look cooler.
Why I call it "pseudo"? Because there is no 3D animation behind, and not even a View animation like rotation around some axis or so. The solution is really simple - I just change the border of the item on gyroscope events which somehow fakes the viewing angle change.
The board item is a simple round rectangle and its border is drawn with a gradient of white color going to transparent. This creates the "fake viewing angle" effect.
So here is our BoardItemView class which is just a simple View:
Code:
public class BoardItemView extends View {
// the color of the board itself (blue)
private int mBoardBack;
// the color of the border (white)
private int mBorderColor;
// the color of the gradient end (transparent)
private int mGradientEndColor;
// constructors
public ToolboxItemView(Context context) {
super(context);
init(context);
}
public ToolboxItemView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
// initialize the colors
Resources r = context.getResources();
mBorderColor = r.getColor(R.color.item_border);
mBoardBack = r.getColor(R.color.item_background);
int transparent = r.getColor(android.R.color.transparent);
setBackgroundColor(transparent);
mGradientEndColor = transparent;
}
As you see, there is nothing special about its initialization. I skipped the other member variables so that it doesn't have any info that is not yet needed for understanding, I will add them later.
Let's go to the onDraw() function:
Code:
...
private static final float RECT_PADDING_PERCENTAGE = 0.05f;
private static final float RECT_RADIUS_PERCENTAGE = 0.1f;
private RectF mRoundRect = new RectF();
private Rect mBounds = new Rect();
private float mRadius = 0.0f;
...
@Override
protected void onDraw(Canvas canvas) {
// step 1: collect information needed for drawing
canvas.getClipBounds(mBounds);
float padding = mBounds.height() * RECT_PADDING_PERCENTAGE;
mRoundRect.set(mBounds);
mRoundRect.inset(padding, padding);
mRadius = RECT_RADIUS_PERCENTAGE * mBounds.height();
...
In the first lines of onDraw() I am taking the clip bounds with getClipBounds() function - I need it to understand where I should do my drawing. My experience showed that it is a good idea to get clip bounds, usually you draw between (0, 0) and (width, height), but I have seen some ugly cases (when I was dealing with Android's Launcher codes) where this is not true.
Then I calculate the round rect parameters, like size and corner radius. As you noticed, no "new" calls in onDraw(), all the needed variables are kept as data members and created when this View is instantiated.
Next comes the drawing of the board itself, nothing special:
Code:
...
private Paint mRectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
...
@Override
protected void onDraw(Canvas canvas) {
...
// step 2: draw the background fill
mRectPaint.setShader(null);
mRectPaint.setDither(false);
mRectPaint.setStyle(Paint.Style.FILL);
mRectPaint.setColor(mBoardBack);
canvas.drawRoundRect(mRoundRect, mRadius, mRadius, mRectPaint);
...
As you can see, I am just drawing a board item as a round rectangle. Before drawing I am setting up the Paint: setting shader to null, dither to false, style to FILL and color to mBoardBack (blue). I will explain the shader and dither in the next step where they are being set. Here I just need to reset (disable) them back. Style is set to FILL so that any shape I paint is also filled with the color of the Paint.
Let's go to the border part, which is interesting:
Code:
...
private LinearGradient mBorderGradient;
...
@Override
protected void onDraw(Canvas canvas) {
...
// step 3: draw the background border
mRectPaint.setStyle(Paint.Style.STROKE);
mRectPaint.setColor(mBorderColor);
createGradient();
mRectPaint.setDither(true);
mRectPaint.setShader(mBorderGradient);
canvas.drawRoundRect(mRoundRect, mRadius, mRadius, mRectPaint);
// step 4: draw the content of a board item here, like text, image, etc...
}
First of all, I am setting the paint style to STROKE. This means that any shape I draw will not be filled with the color, only the border will be drawn. There is also FILL_AND_STROKE style which both draws the border of the shape and fills it with the paint (remember that in previous step we just filled the round rectangle). Since I am not setting the width of the border stroking it will be just one pixel wide. This is enough to see the effect and not big enough for eyes to see the "pseudo"-ness of the 3D effect.
After that I am setting the color of the Paint and then calling a createGradient() function. We will come to that function in a few minutes. Then I am enabling the dither mode on the Paint and setting a shader on it to be the gradient that I just created with that createGradient() function call.
What does all that mean and what is a Shader in Android's Paint system? Its basically quite simple - a Shader is an object from where the Paint gets color information during drawing any shape (except drawing bitmaps). When the shader is null then the Paint object uses the color it was set, otherwise it asks the Shader object what color to use when painting a pixel at some coordinate. As an example you can see a picture acting as a shader and what will happen if a Paint will draw letter 'R' using that shader.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Seems like there are fixed number of shaders in Android, although BitmapShader is covering almost all of the possibilities. In my case I use LinearGradient class which extends Shader class.
TO BE CONTINUED in the thread, seems there is limit on post size...
...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:
Code:
...
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.
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:
The final step is to set the "view angle" based on gyroscope values:
Code:
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:
Code:
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:
Direct link:
Download
It would be nice to get some comments and suggestions, let me know your thoughts!
Related
Can someone make a .cab or an .exe program to calculate Weight Watchers points? I found this article on the internet where it looks like someone as already done it but they did not post it. Here is the article
http://geekswithblogs.net/cdahlinge...-mobile-meets-weight-watchers--mvp-style.aspx
Craig Dahlinger
<< Presenting at Richdmond code camp 2008.2 | Home | mshtml β the ongoing adventure >> windows mobile meets weight watchers : MVP style Ok, so I know it has been a long time since a post, but it has been really busy with work and family. I have been busy coding and learning lots of new stuff. I work with a great bunch of developers and my current team lead is a great mentor.
Well for the new year the wife and I decided to get back into shape. I started hitting the gym and so did she but she is also doing weight watchers with a friend. One of the things they do is they have to calculate points on a daily basis. These points are comprised of calories, fat and fiber. There is a formula for these three which in turn results in the number of points a particular item is. A few months ago I convinced the wife to get a windows mobile device (woo hoo!) and she is a good power user. So one night she asks me, βIs there a way I can just enter in the calories, fat and fiber on my phone and it tell me how many points something is?β. I did some searching and there are numerous online versions of the calculator but no native ones for windows mobile. I found the formula here, and started to get to work.
I wanted to approach this application using the MVP design pattern. I know it may be overkill for this simple of an application but I thought it would be good practice.
I started with the interface for the data model, in this case it would be the main caloric properties of food.
namespace WWPC.Common.Interfaces{ public interface IFoodModel { int Fiber { get; set; } int Calories { get; set; } float Fat { get; set; } int Points { get; set; } int CalculatePoints(); }}I then wrote up the interface for the view for the model.
namespace WWPC.Common.Interfaces{ public interface IFoodCalcView { int Calories { get; } int Fiber { get; } float Fat { get; } int Points { set; } event EventHandler DataChanged; }}Next, came the interface for the presenter.
public interface IFoodCalcPresenter { void OnCalculatePoints(); }
Ok, now that I got my main interfaces in place, time to code up the implementation. I started with the model first since this was the class that would provide the implementation for calculating the caloric points. Using the formula mentioned above, the CalculatePoints() method came out like so:
public int CalculatePoints(){ var calories = Convert.ToDecimal(Calories); var cal = calories / 50; var totalFat = Convert.ToDecimal(Fat); var fat = totalFat / 12; var fiber = Convert.ToDecimal(Fiber); return Points = Convert.ToInt32(Math.Round(cal + fat - (fiber/5), 0)); } With the model complete, I then moved to the presenter. The presenter would be responsible for binding the model to the view responding to the data changes in the view and rebinding those changes to the model. I made the presenter with an overloaded constructor to take a view and a model. The presenter then binds to the data changed event on the view which enables the presenter to update the model from the view. The OnCalculatePoints() method will update the view with the points value after using the model for calculation.
namespace WWPC.Common{ public class FoodPresenter : IFoodCalcPresenter { private readonly IFoodCalcView _View; private readonly IFoodModel _Model; public FoodPresenter(IFoodCalcView view, IFoodModel model) { _View = view; _View.DataChanged += new EventHandler(_View_DataChanged); _Model = model; } void _View_DataChanged(object sender, EventArgs e) { SetModelFromView(); } private void SetModelFromView() { _Model.Calories = _View.Calories; _Model.Fat = _View.Fat; _Model.Fiber = _View.Fiber; } #region IFoodCalcPresenter Members public void OnCalculatePoints() { _View.Points = _Model.CalculatePoints(); } #endregion }}
With the presenter done it was time to implement the view. I wanted a simple mobile form where you can enter in data quickly and then calculate the results. I initially tried using a label to display the result, but did not like it. I then tried a mobile gauge control, but that took up too much space on the small screen. Finally I decided to use the notification class for windows mobile. I did not use the managed wrapper version, I used the the version created by Christopher Fairbairn, found here. This version has an awesome implementation which exposes many features of the notification class. I wanted to give the user the ability to dismiss the notification when they were done reading the results. Also using the notification class the UI was able show the needed text boxes for entry and the SIP panel along with the results without needing to scroll the screen. Here is a screen shot of the main form.
Now with the controls in place on the form, I can implement the view. The form creates a new presenter and passed into it a new model during construction. When the calculate menu option is clicked the main form raises the data changed event then calls the OnCalculateMethod on the presenter. When the presenter binds the model to the view, during the set of the points value, the notification is shown to the user via the ShowNotification method.
namespace WWPC.Calc{ public partial class WWPCalculator : Form, IFoodCalcView { private readonly FoodPresenter _Presenter; private NotificationWithSoftKeys _Notification; public WWPCalculator() { InitializeComponent(); _Presenter = new FoodPresenter(this,new FoodModel()); } public int Calories { get { return (string.IsNullOrEmpty(txtCalories.Text)) ? 0 : Int32.Parse(txtCalories.Text); } } public int Fiber { get { return (cmbFiber.Text == "4 or more") ? 4 : (string.IsNullOrEmpty(cmbFiber.Text)) ? 0 :Int32.Parse(cmbFiber.Text); } } public float Fat { get { return (string.IsNullOrEmpty(txtFat.Text)) ? 0 : float.Parse(txtFat.Text); } } public int Points { set { ShowPointsNotification(value); } } public event EventHandler DataChanged; private void mnuExit_Click(object sender, EventArgs e) { this.Close(); } private void mnuCalculate_Click(object sender, EventArgs e) { if (DataChanged != null) this.DataChanged(sender, e); _Presenter.OnCalculatePoints(); } private void mnuClear_Click(object sender, EventArgs e) { txtCalories.Text = string.Empty; txtFat.Text = string.Empty; cmbFiber.Text = "0"; } private void ShowPointsNotification(int points) { _Notification = new NotificationWithSoftKeys { Text = String.Format("Total Points:{0}", points), Caption = "Weight Watchers Point Calculator", RightSoftKey = new NotificationSoftKey(SoftKeyType.Dismiss, "Dismiss"), }; _Notification.RightSoftKeyClick+=new EventHandler(_Notification_RightSoftKeyClick); _Notification.Visible = true; } void _Notification_RightSoftKeyClick(object sender, EventArgs e) { if (_Notification == null) return; _Notification.Visible = false; _Notification = null; } }}
Now, when it is all put together, it looks like so.
Below is a link to the source code. The project was done using Visual Studio 2008 against the windows mobile 5 sdk. It will also work against windows mobile 6 sdk, I just chose version 5 since that is the common sdk. Thanks for reading!!
Hello to all!
I have not participated much in the forum, though I have been reading you for a while So... I am going to ask for your help
I'm trying to make a custom paint with the finger (to draw, to take notes, whatever) and I'm having some troubles.
Up to now, from what I have read, I'm using a class extended from View implementing OnTouchListener , and using the onDraw() method, with the onTouch event. I detect when the screen is touched (pressed, or dragged to "paint" in the screen) with the onTouch method, store the last point in an array, and in the onDraw method, I paint all the array.
-As my knowledge goes, in the onDraw method, you have to re-paint all the canvas (this is translated to my problem, to repaint all the stroke or "painting"). Is this correct or I am doing anything wrong? Is there any way to only update the current canvas, and not have to re-paint all the screen? Maybe using othe classes or other methods, but I have found nothing in the net (maybe I'm not looking in the correct places).
- The second problem I found is that when I drag the finger across the screen, sometimes the ontouch method doesen't capture all the points I have "touched" with the finger. I mean, I don't want to capture ALL the points, but when I drag the finger with moderate speed (not very fast) there are important gaps in between. Is there a way to optimize this and try to capture more points in between?
So far, this is the code I have relevant to the issues I'm explaining:
I'm trying to copy the code but I get a error (It sais I can not post outside links, but I'm not posting any links, just the code)
Code:
private ArrayList<PointStroke > stroke= new ArrayList<PointStroke >();
protected void onDraw(Canvas canvas) {
Paint p = new Paint();
p.setColor(Color.RED);
int signLength = stroke.size();
PointStroke pf;
PointStroke pfant;
for (int index = 0; index < signLength; index++) {
pf = stroke.get(index);
canvas.drawCircle(pf.x,pf.y,7,p);
if (index > 0 ) {
pfant = stroke.get(index-1);
canvas.drawLine(pfant.x, pfant.y, pf.x, pf.y, p);
}
}
canvas.drawCircle(x,y,7,p);
invalidate();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
x = event.getX();
y = event.getY();
pr = event.getPressure();
sz = event.getSize();
long now = System.currentTimeMillis();
stroke.add(new PointStroke(x,y,pr,sz,now));
return true;
}
private class PointStroke{
public float x;
public float y;
public float pr;
public float sz;
public long time;
public PointStroke(float newX, float newY, float newPR, float newSZ, long newTIME) {
x = newX;
y = newY;
pr = newPR;
sz = newSZ;
time = newTIME;
}
}
Thanks for all your answers and the time to read and answer!
I want to share some codes of a small effect that I implemented in my Android app called Arithmetic Puzzles. This is also a chance for me to listen to other people and make improvements. At the end of this post there is a link to the app so that you can see the code in action.
It is a text shining effect which reacts on device movements. It creates a feeling of glass surface of the text which shines and reflects light. Only outline of the text is shining.
Please note that the text in my case was very short - a number with 2 digits - which looks cool. If you will try a longer text then let me know how it looks
I grabbed some parts of my code from its context to put here so if there is something missing or irrelevant then just let me know.
So, here we go! The text shine is done extending a simple View:
Code:
public class EquationView extends View {
...
// the text to be drawn
private String mText;
private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint mTextShinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mShineColor;
private int mShineNoColor;
...
// constructors
public EquationView(Context context) {
super(context);
init(context);
}
public EquationView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public void initialize(Context context) {
// paints
setupPaint(mTextPaint, R.color.text_color, context); // text color (yellow for me)
setupPaint(mTextShinePaint, R.color.text_shine, context); // shine color (white for me)
mTextShinePaint.setDither(true);
mTextShinePaint.setStyle(Paint.Style.STROKE);
mTextShinePaint.setStrokeJoin(Paint.Join.ROUND);
mTextShinePaint.setStrokeMiter(10);
// colors
mTextShadowColor = context.getResources().getColor(R.color.text_shadow); (#AA454210 for me)
mShineColor = context.getResources().getColor(R.color.text_shine); (white for me)
mShineNoColor = context.getResources().getColor(android.R.color.transparent);
}
private void setupPaint(Paint paint, int colorId, Context context) {
paint.setColor(context.getResources().getColor(colorId));
paint.setTextAlign(Paint.Align.CENTER);
}
The initialization function is setting up the paint objects. Since I am going to use LinearGradient as a shader in my paint, I am setting the dither to true. I have already wrote about it in my previous guide. I am also setting the shine paint style to STROKE so that only the outline of the text is "shining".
The stroke parameters like join and miter are mostly set to make it look prettier.
I also set the text align to CENTER. This has effect during drawing of the text - when I tell the paint to draw a text at some (x, y) point, then x is considered as the center point of the whole text, thus text is centered horizontally on this origin.
Next, let's have a look at the onDraw() function:
Code:
...
private static final float TEXT_HEIGHT = 0.8f;
private static final float SHINE_THICKNESS = 0.015f;
private final float mShadowBlurRadius = 5.0f * getResources().getDisplayMetrics().density; // 5dp
private LinearGradient mShineGradient;
private int mTextShadowColor;
private float mShadowShiftX = 0.0f;
private float mShadowShiftY = 0.0f;
private Rect mBounds = new Rect();
private Rect mTextBounds = new Rect();
...
@Override
protected void onDraw(Canvas canvas) {
// step 1. collect information needed for drawing
canvas.getClipBounds(mBounds);
float centerX = mBounds.centerX();
float h = mBounds.height();
float textSize = h * TEXT_HEIGHT;
float textCenterY = mBounds.top + h * 0.5f;
// step 2. draw the shadows
mTextPaint.setShadowLayer(mShadowBlurRadius, mShadowShiftX, mShadowShiftY, mTextShadowColor);
drawText(mText, centerX, textCenterY, textSize, canvas, mTextPaint);
// step 3. draw the shine
if (mShineGradient != null) {
mTextShinePaint.setShader(mShineGradient);
mTextShinePaint.setStrokeWidth(TextSize * SHINE_THICKNESS);
drawText(mText, centerX, textCenterY, textSize, canvas, mTextShinePaint);
}
// step 4. draw the text
mTextPaint.clearShadowLayer();
drawText(mText, centerX, textCenterY, textSize, canvas, mTextPaint);
}
private void drawText(String text, float centerX, float centerY,
float size, Canvas canvas, Paint paint) {
paint.setTextSize(size);
paint.getTextBounds(text, 0, text.length(), mTextBounds);
canvas.drawText(text, centerX, centerY + mTextBounds.height() / 2.0f - mTextBounds.bottom, paint);
}
In step 1 I collect information for drawing like clip bounds (see previous guide for clip bounds), sizes and positions.
In step 2 I draw the shadows. I also want the shadows to move when device moves (on gyroscope events). I actually draw the text with shadows - the text will be overdrawn in next steps so only shadows will be left from this step. The offsets of the shadows - mShadowShiftX and mShadowShiftY - are updated based on gyroscope data.
In step 3 I draw the shine. I set the LinearGradient mShineGradient as the shader of the paint and then set the stroke width to SHINE_THICKNESS. Then I draw text with that shader. For more information on shaders see my previous guide. The mShineGradient is updated when new gyroscope data is received. We will come to this gradient creation later.
In step 4 I disable the shadows and draw the text again. It will overwrite only the text, not the shadows and not the shine, so I have only the outline shining.
A common drawText() function is used to draw the text. It first sets the text size (font size), then calculates the text bounds using getTextBounds(). This is needed to center the text around the origin point also in vertical direction since Paint.Align.CENTER is aligning only in horizontal direction.
TO BE CONTINUED in the thread, seems there is limit on post size...
CONTINUATION of the guide
Now lets see how is the mShineGradient created. This is done every time we got a new data from gyroscope:
Code:
...
// 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:
Code:
...
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:
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
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:
Direct link:
Download
It would be nice to get some comments and suggestions, let me know your thoughts!
Hello,
I create that thread to present you a tutorial aiming to learn you how to create a Running Man Game Animation on Android. You can discover the tutorial in video on Youtube :
Create a Running Man Game Animation on Android
Making a Running Man Animation on Android is a great way to learn how to work with Bitmaps, Thread and SurfaceView. First thing to make a Running Man Game Animation is to have a character to animate. For that, we will use the following sprite sheet :
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Like you can see, our character sprite sheet has 8 frames. Each frame show the character in a different position when he runs.
For our animation, we are going to create a custom GameView extending the SurfaceView class and implementing the Runnable interface. First, we need to define some properties like the game thread, the surface holder, the canvas where running man will be drawn, the bitmap used to load the sprite sheet and some parameters to customize the running man animation like the speed, the size or the position of the man on the screen :
Code:
class GameView extends SurfaceView implements Runnable {
private Thread gameThread;
private SurfaceHolder ourHolder;
private volatile boolean playing;
private Canvas canvas;
private Bitmap bitmapRunningMan;
private boolean isMoving;
private float runSpeedPerSecond = 500;
private float manXPos = 10, manYPos = 10;
private int frameWidth = 230, frameHeight = 274;
private int frameCount = 8;
private int currentFrame = 0;
private long fps;
private long timeThisFrame;
private long lastFrameChangeTime = 0;
private int frameLengthInMillisecond = 50;
// ...
}
To draw correctly the good frame for the running man, we need two Rectangle instances. One used to define the current frame in the sprite sheet and an other to define where to draw the current frame on the screen :
Code:
private Rect frameToDraw = new Rect(0, 0, frameWidth, frameHeight);
private RectF whereToDraw = new RectF(manXPos, manYPos,
manXPos + frameWidth, frameHeight);
On the GameView constructor, we get the surface holder and then, we load the sprite sheet into the bitmapRunningMan variable. We apply a scale transformation according values defined in frameWidth and frameHeight parameters :
Code:
public GameView(Context context) {
super(context);
ourHolder = getHolder();
bitmapRunningMan = BitmapFactory.decodeResource(getResources(),
R.drawable.running_man);
bitmapRunningMan = Bitmap.createScaledBitmap(bitmapRunningMan,
frameWidth * frameCount, frameHeight, false);
}
Now, it's time to make the event loop for animation inside the run method overrided from Runnable interface :
Code:
@Override
public void run() {
while (playing) {
long startFrameTime = System.currentTimeMillis();
update();
draw();
timeThisFrame = System.currentTimeMillis() - startFrameTime;
if (timeThisFrame >= 1) {
fps = 1000 / timeThisFrame;
}
}
}
Note that we animate the character while the playing variable is set to true. Like usual in a game, we update the elements and then we draw before to calculate frame per seconds. The update method is just used here to move the man positions in X and Y. Note that when the man reach the end of the screen horizontally or vertically, we set its position to the left or top of the screen :
Code:
public void update() {
if (isMoving) {
manXPos = manXPos + runSpeedPerSecond / fps;
if (manXPos > getWidth()) {
manYPos += (int) frameHeight;
manXPos = 10;
}
if (manYPos + frameHeight > getHeight()) {
manYPos = 10;
}
}
}
Before to write the draw method, we need to define a method to manage the current frame to display for the character. We change the current frame only when he have ended the frame duration :
Code:
public void manageCurrentFrame() {
long time = System.currentTimeMillis();
if (isMoving) {
if (time > lastFrameChangeTime + frameLengthInMillisecond) {
lastFrameChangeTime = time;
currentFrame++;
if (currentFrame >= frameCount) {
currentFrame = 0;
}
}
}
frameToDraw.left = currentFrame * frameWidth;
frameToDraw.right = frameToDraw.left + frameWidth;
}
And now, we define the draw method :
Code:
public void draw() {
if (ourHolder.getSurface().isValid()) {
canvas = ourHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
whereToDraw.set((int) manXPos, (int) manYPos, (int) manXPos
+ frameWidth, (int) manYPos + frameHeight);
manageCurrentFrame();
canvas.drawBitmap(bitmapRunningMan, frameToDraw, whereToDraw, null);
ourHolder.unlockCanvasAndPost(canvas);
}
}
First, we check if the surface is valid. Then we lock the canvas and we draw the character current frame. Last, we unlock the canvas and post it on the Surface View. Finally, we define two methods to pause or resume the running man animation :
Code:
public void pause() {
playing = false;
try {
gameThread.join();
} catch(InterruptedException e) {
Log.e("ERR", "Joining Thread");
}
}
public void resume() {
playing = true;
gameThread = new Thread(this);
gameThread.start();
}
To start the running man animation, we're going to wait the user click on the surface view. So, we need to override the onTouchEvent method and wait for an ACTION_DOWN event. When the event is made, we have just to change the isMoving boolean value. If man is running, we stop it. If man doesn't run, we start to move it :
Code:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN :
isMoving = !isMoving;
break;
}
return true;
}
Last thing to make is to assemble all the pieces of the puzzle, create the game view on the main activity, set it as the content view and then resume or pause the game animation when the activity is resumed or paused :
Code:
package com.ssaurel.runningman;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class RunningManAnimation extends AppCompatActivity {
private GameView gameView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameView = new GameView(this);
setContentView(gameView);
}
@Override
protected void onResume() {
super.onResume();
gameView.resume();
}
@Override
protected void onPause() {
super.onPause();
gameView.pause();
}
class GameView extends SurfaceView implements Runnable {
private Thread gameThread;
private SurfaceHolder ourHolder;
private volatile boolean playing;
private Canvas canvas;
private Bitmap bitmapRunningMan;
private boolean isMoving;
private float runSpeedPerSecond = 500;
private float manXPos = 10, manYPos = 10;
private int frameWidth = 230, frameHeight = 274;
private int frameCount = 8;
private int currentFrame = 0;
private long fps;
private long timeThisFrame;
private long lastFrameChangeTime = 0;
private int frameLengthInMillisecond = 50;
private Rect frameToDraw = new Rect(0, 0, frameWidth, frameHeight);
private RectF whereToDraw = new RectF(manXPos, manYPos, manXPos + frameWidth, frameHeight);
public GameView(Context context) {
super(context);
ourHolder = getHolder();
bitmapRunningMan = BitmapFactory.decodeResource(getResources(), R.drawable.running_man);
bitmapRunningMan = Bitmap.createScaledBitmap(bitmapRunningMan, frameWidth * frameCount, frameHeight, false);
}
@Override
public void run() {
while (playing) {
long startFrameTime = System.currentTimeMillis();
update();
draw();
timeThisFrame = System.currentTimeMillis() - startFrameTime;
if (timeThisFrame >= 1) {
fps = 1000 / timeThisFrame;
}
}
}
public void update() {
if (isMoving) {
manXPos = manXPos + runSpeedPerSecond / fps;
if (manXPos > getWidth()) {
manYPos += (int) frameHeight;
manXPos = 10;
}
if (manYPos + frameHeight > getHeight()) {
manYPos = 10;
}
}
}
public void manageCurrentFrame() {
long time = System.currentTimeMillis();
if (isMoving) {
if (time > lastFrameChangeTime + frameLengthInMillisecond) {
lastFrameChangeTime = time;
currentFrame++;
if (currentFrame >= frameCount) {
currentFrame = 0;
}
}
}
frameToDraw.left = currentFrame * frameWidth;
frameToDraw.right = frameToDraw.left + frameWidth;
}
public void draw() {
if (ourHolder.getSurface().isValid()) {
canvas = ourHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
whereToDraw.set((int) manXPos, (int) manYPos, (int) manXPos + frameWidth, (int) manYPos + frameHeight);
manageCurrentFrame();
canvas.drawBitmap(bitmapRunningMan, frameToDraw, whereToDraw, null);
ourHolder.unlockCanvasAndPost(canvas);
}
}
public void pause() {
playing = false;
try {
gameThread.join();
} catch(InterruptedException e) {
Log.e("ERR", "Joining Thread");
}
}
public void resume() {
playing = true;
gameThread = new Thread(this);
gameThread.start();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN :
isMoving = !isMoving;
break;
}
return true;
}
}
}
Now, you have just to run the application on your Android emulator or on your real device and to enjoy your Running Man Game Animation.
Don't hesitate to try this code and give me your feedbacks.
Thanks.
Sylvain
I just started learning self taught Android development and hopefully, I get to this level one days. Thanks for sharing your steps for this
Sent from my Nexus 6P using Tapatalk
nighthawk626 said:
I just started learning self taught Android development and hopefully, I get to this level one days. Thanks for sharing your steps for this
Sent from my Nexus 6P using Tapatalk
Click to expand...
Click to collapse
Great . I plan to make some tutorials for beginners too. Do you have some suggestions for that kind of tutorials ?
No suggestions, I'm just looking for a good place to begin. I first started with Android development then I scaled back to learn about Java, and still no luck, so I just started off with c++. Lol
Sent from my Nexus 6P using Tapatalk
Hello,
I create that thread to present you a tutorial learning to draw an Analog Clock on Android with the Canvas 2D API. It is also a good introduction to Canvas API on Android. You can enjoy this tutorial in video on Youtube too :
To learn to use the Canvas 2D API, a good exercise is to draw an Analog Clock on Android. In this tutorial, you are going to discover how to create this kind of Clock and you will see some basics like creating a custom view and draw on a Canvas.
The first step of the tutorial is to create a custom ClockView which will be used to draw the Analog Clock on the screen. To start, you need to define an initClock() method where you get the height and the width of the custom view. The minimum between the height and the width will be used to determine the dimensions of the square nesting the Analog Clock.
For the Analog Clock, we will define the radius by dividing the min between the height and the width by two. We subtract a padding value used to add some padding for the Analog Clock.
To draw the Analog Clock, we need to override the onDraw() method of our ClockView. If the view is not initialized, we call the initClock(). Then, we call the methods defined to draw the circle of the clock, the center, the numerals and the hands.
To update the time displayed on the Clock, we need to invalidate the view. Thus, it is drawn each 500 milliseconds.
It gives us the following code for the ClockView :
Code:
package com.ssaurel.canvasclock;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import java.util.Calendar;
public class ClockView extends View {
private int height, width = 0;
private int padding = 0;
private int fontSize = 0;
private int numeralSpacing = 0;
private int handTruncation, hourHandTruncation = 0;
private int radius = 0;
private Paint paint;
private boolean isInit;
private int[] numbers = {1,2,3,4,5,6,7,8,9,10,11,12};
private Rect rect = new Rect();
public ClockView(Context context) {
super(context);
}
public ClockView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void initClock() {
height = getHeight();
width = getWidth();
padding = numeralSpacing + 50;
fontSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 13,
getResources().getDisplayMetrics());
int min = Math.min(height, width);
radius = min / 2 - padding;
handTruncation = min / 20;
hourHandTruncation = min / 7;
paint = new Paint();
isInit = true;
}
@Override
protected void onDraw(Canvas canvas) {
if (!isInit) {
initClock();
}
canvas.drawColor(Color.BLACK);
drawCircle(canvas);
drawCenter(canvas);
drawNumeral(canvas);
drawHands(canvas);
postInvalidateDelayed(500);
}
private void drawHand(Canvas canvas, double loc, boolean isHour) {
double angle = Math.PI * loc / 30 - Math.PI / 2;
int handRadius = isHour ? radius - handTruncation - hourHandTruncation : radius - handTruncation;
canvas.drawLine(width / 2, height / 2,
(float) (width / 2 + Math.cos(angle) * handRadius),
(float) (height / 2 + Math.sin(angle) * handRadius),
paint);
}
private void drawHands(Canvas canvas) {
Calendar c = Calendar.getInstance();
float hour = c.get(Calendar.HOUR_OF_DAY);
hour = hour > 12 ? hour - 12 : hour;
drawHand(canvas, (hour + c.get(Calendar.MINUTE) / 60) * 5f, true);
drawHand(canvas, c.get(Calendar.MINUTE), false);
drawHand(canvas, c.get(Calendar.SECOND), false);
}
private void drawNumeral(Canvas canvas) {
paint.setTextSize(fontSize);
for (int number : numbers) {
String tmp = String.valueOf(number);
paint.getTextBounds(tmp, 0, tmp.length(), rect);
double angle = Math.PI / 6 * (number - 3);
int x = (int) (width / 2 + Math.cos(angle) * radius - rect.width() / 2);
int y = (int) (height / 2 + Math.sin(angle) * radius + rect.height() / 2);
canvas.drawText(tmp, x, y, paint);
}
}
private void drawCenter(Canvas canvas) {
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(width / 2, height / 2, 12, paint);
}
private void drawCircle(Canvas canvas) {
paint.reset();
paint.setColor(getResources().getColor(android.R.color.white));
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
canvas.drawCircle(width / 2, height / 2, radius + padding - 10, paint);
}
}
Once the ClockView is created, we have just to add it in the layout of our main activity :
Code:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.ssaurel.canvasclock.MainActivity"
android:background="@android:color/black"
>
<com.ssaurel.canvasclock.ClockView
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerInParent="true"/>
</RelativeLayout>
The code of the Main Activity is simple. We have just to set this layout as the content view :
Code:
package com.ssaurel.canvasclock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Now, you can try your Analog Clock application and you should enjoy the following result :
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Don't hesitate to give it a try and give me your feedbacks.
Thanks.
Sylvain
thank you soo much