Custom Knob View for controlling Volume? - android

I want to show progress bar around knob. After following this tutorial I created this
knob it is working fine.
But How could i modify the above knob to look like the second image
Running code for the first knob is written below.
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.View.MeasureSpec;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.RelativeLayout;
public class RoundKnobButton extends RelativeLayout implements
OnGestureListener
{
public int eventValue=10;
//doctory starts
Paint p1,p2,p3;
RectF oval;
int width;
//doctory ends
private GestureDetector gestureDetector;
private float mAngleDown, mAngleUp;
private ImageView ivRotor;
private Bitmap bmpRotorOn, bmpRotorOff;
private boolean mState = false;
private int m_nWidth = 0, m_nHeight = 0;
public interface RoundKnobButtonListener
{
public void onStateChange(boolean newstate);
public void onRotate(int percentage);
}
private RoundKnobButtonListener m_listener;
public void SetListener(RoundKnobButtonListener l)
{
m_listener = l;
}
public void SetState(boolean state)
{
mState = state;
ivRotor.setImageBitmap(state ? bmpRotorOn : bmpRotorOff);
}
public RoundKnobButton(Context context, int back, int rotoron,
int rotoroff, final int w, final int h) {
super(context);
//doctory starts
width = w;
p1 = new Paint(1);
p1.setColor(Color.rgb(86, 86, 86));
p1.setStyle(android.graphics.Paint.Style.FILL);
p2 = new Paint(1);
p2.setColor(Color.rgb(245, 109, 89));
p2.setStyle(android.graphics.Paint.Style.FILL);
p3 = new Paint(1);
p3.setColor(Color.GREEN);
p3.setStyle(android.graphics.Paint.Style.STROKE);
oval = new RectF();
//doctory ends...
// we won't wait for our size to be calculated, we'll just store out
// fixed size
m_nWidth = w;
m_nHeight = h;
// create stator
ImageView ivBack = new ImageView(context);
ivBack.setImageResource(back);
RelativeLayout.LayoutParams lp_ivBack = new RelativeLayout.LayoutParams(
w, h);
lp_ivBack.addRule(RelativeLayout.CENTER_IN_PARENT);
addView(ivBack, lp_ivBack);
// load rotor images
Bitmap srcon = BitmapFactory.decodeResource(context.getResources(),
rotoron);
Bitmap srcoff = BitmapFactory.decodeResource(context.getResources(),
rotoroff);
float scaleWidth = ((float) w) / srcon.getWidth();
float scaleHeight = ((float) h) / srcon.getHeight();
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
bmpRotorOn = Bitmap.createBitmap(srcon, 0, 0, srcon.getWidth(),
srcon.getHeight(), matrix, true);
bmpRotorOff = Bitmap.createBitmap(srcoff, 0, 0, srcoff.getWidth(),
srcoff.getHeight(), matrix, true);
// create rotor
ivRotor = new ImageView(context);
ivRotor.setImageBitmap(bmpRotorOn);
RelativeLayout.LayoutParams lp_ivKnob = new RelativeLayout.LayoutParams(
w, h);// LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
lp_ivKnob.addRule(RelativeLayout.CENTER_IN_PARENT);
addView(ivRotor, lp_ivKnob);
// set initial state
SetState(mState);
// enable gesture detector
gestureDetector = new GestureDetector(getContext(), this);
}
/**
* math..
*
* #param x
* #param y
* #return
*/
private float cartesianToPolar(float x, float y) {
return (float) -Math.toDegrees(Math.atan2(x - 0.5f, y - 0.5f));
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event))
return true;
else
return super.onTouchEvent(event);
}
public boolean onDown(MotionEvent event)
{
float x = event.getX() / ((float) getWidth());
float y = event.getY() / ((float) getHeight());
mAngleDown = cartesianToPolar(1 - x, 1 - y);// 1- to correct our custom
// axis direction
return true;
}
public boolean onSingleTapUp(MotionEvent e)
{
float x = e.getX() / ((float) getWidth());
float y = e.getY() / ((float) getHeight());
mAngleUp = cartesianToPolar(1 - x, 1 - y);// 1- to correct our custom
// axis direction
// if we click up the same place where we clicked down, it's just a
// button press
if (!Float.isNaN(mAngleDown) && !Float.isNaN(mAngleUp)
&& Math.abs(mAngleUp - mAngleDown) < 10) {
SetState(!mState);
if (m_listener != null)
m_listener.onStateChange(mState);
}
return true;
}
public void setRotorPosAngle(float deg)
{
if (deg >= 210 || deg <= 150)
{
if (deg > 180)
deg = deg - 360;
Matrix matrix = new Matrix();
ivRotor.setScaleType(ScaleType.MATRIX);
// matrix.postRotate((float) deg, 210 / 2, 210 / 2);// getWidth()/2,
// getHeight()/2);
matrix.postRotate((float) deg, m_nWidth/2, m_nHeight/2);//getWidth()/2, getHeight()/2);
ivRotor.setImageMatrix(matrix);
}
}
public void setRotorPercentage(int percentage)
{
int posDegree = percentage * 3 - 150;
if (posDegree < 0)
posDegree = 360 + posDegree;
setRotorPosAngle(posDegree);
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
float x = e2.getX() / ((float) getWidth());
float y = e2.getY() / ((float) getHeight());
float rotDegrees = cartesianToPolar(1 - x, 1 - y);// 1- to correct our
// custom axis
// direction
if (!Float.isNaN(rotDegrees)) {
// instead of getting 0-> 180, -180 0 , we go for 0 -> 360
float posDegrees = rotDegrees;
if (rotDegrees < 0)
posDegrees = 360 + rotDegrees;
// deny full rotation, start start and stop point, and get a linear
// scale
if (posDegrees > 210 || posDegrees < 150) {
// rotate our imageview
setRotorPosAngle(posDegrees);
// get a linear scale
float scaleDegrees = rotDegrees + 150; // given the current
// parameters, we go
// from 0 to 300
// get position percent
int percent = (int) (scaleDegrees / 3);
if (m_listener != null)
m_listener.onRotate(percent);
return true; // consumed
} else
return false;
} else
return false; // not consumed
}
public void onShowPress(MotionEvent e) {
// TODO Auto-generated method stub
}
public boolean onFling(MotionEvent arg0, MotionEvent arg1, float arg2,
float arg3) {
return false;
}
public void onLongPress(MotionEvent e) {
}
/*#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
int i = width / 4;
oval.set(i - i / 2, i / 2, i * 3 + i / 2, i * 3 + i / 2);
canvas.drawOval(oval, p1);
canvas.drawArc(oval, -90F, (int)Math.round((360D * Double.valueOf(eventValue).doubleValue()) / 100D), true, p2);
canvas.drawLine(20, 30, 120, 200, p2);
}*/
/*#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int desiredWidth = width;
int desiredHeight = width;
int widthMode = android.view.View.MeasureSpec.getMode(widthMeasureSpec);
int widthSize = android.view.View.MeasureSpec.getSize(widthMeasureSpec);
int heightMode = android.view.View.MeasureSpec.getMode(heightMeasureSpec);
int heightSize = android.view.View.MeasureSpec.getSize(heightMeasureSpec);
int measuredWidth;
int measuredHeight;
if (widthMode == MeasureSpec.EXACTLY)
{
measuredWidth = widthSize ;
}
else if (widthMode == MeasureSpec.AT_MOST)
{
measuredWidth = Math.min(desiredWidth , widthSize);
}
else
{
measuredWidth = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY)
{
measuredHeight = heightSize ;
}
else if (heightMode == MeasureSpec.AT_MOST)
{
measuredHeight = Math.min(desiredHeight, heightSize );
}
else
{
measuredHeight = desiredHeight;
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
*/
}
I have created onDraw and onMeasure function function and try to draw outer progress, or how could i modify it.
Edit
Is it possible to change the rotation percentage to 20 . I mean it shows progress form 0 to 99. is it possible to convert it to 0 to 12.

Use two XML files.
circular_progress_drawable.xml
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="270"
android:toDegrees="270">
<shape
android:innerRadiusRatio="2.5"
android:shape="ring"
android:thickness="1dp">
<gradient
android:angle="0"
android:endColor="#22FA05"
android:startColor="#22FA05"
android:type="sweep"
android:useLevel="false" />
</shape>
</rotate>
and background_circle.xml
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:innerRadiusRatio="2.5"
android:thickness="1dp"
android:useLevel="false">
<solid android:color="#000000" />
</shape>
and finally
<ProgressBar
android:id="#+id/progressBar"
android:layout_width="250dp"
android:layout_height="250dp"
android:indeterminate="false"
android:progressDrawable="#drawable/circular_progress_drawable"
android:background="#drawable/background_circle"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
android:progress="25" />
Note that set width and height based on the image position you have

Related

How can I make the background of my custom brush transparent?

I wanted to draw on the canvas with the code below with my custom brush, but as you can see in the picture, the background of my brush is black, albeit without color.
Although I specified the brush color as Color.TRANSPARENT or Color.parseColor ("# 00000000"), the brush background still turns black.
How can I make the background color of my brush transparent?
click to see the picture
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
import java.util.Stack;
public class BrushDrawingView extends View {
static final float DEFAULT_BRUSH_SIZE = 50.0f;
static final float DEFAULT_ERASER_SIZE = 50.0f;
static final int DEFAULT_OPACITY = 255;
private float mBrushSize = DEFAULT_BRUSH_SIZE;
private float mBrushEraserSize = DEFAULT_ERASER_SIZE;
private int mOpacity = DEFAULT_OPACITY;
private final Stack<BrushLinePath> mDrawnPaths = new Stack<>();
private final Stack<BrushLinePath> mRedoPaths = new Stack<>();
private final Paint mDrawPaint = new Paint();
private Canvas mDrawCanvas;
private boolean mBrushDrawMode;
private Bitmap brushBitmap;
private Path mPath;
private float mTouchX, mTouchY;
private static final float TOUCH_TOLERANCE = 4;
private BrushViewChangeListener mBrushViewChangeListener;
public BrushDrawingView(Context context) {
this(context, null);
}
public BrushDrawingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BrushDrawingView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setupBrushDrawing();
}
private void setupBrushDrawing() {
//Caution: This line is to disable hardware acceleration to make eraser feature work properly
setupPathAndPaint();
setVisibility(View.GONE);
}
private void setupPathAndPaint() {
mPath = new Path();
mDrawPaint.setAntiAlias(true);
mDrawPaint.setStyle(Paint.Style.STROKE);
mDrawPaint.setStrokeJoin(Paint.Join.ROUND);
mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
mDrawPaint.setStrokeWidth(mBrushSize);
mDrawPaint.setAlpha(mOpacity);
}
private void refreshBrushDrawing() {
mBrushDrawMode = true;
setupPathAndPaint();
}
void brushEraser() {
mBrushDrawMode = true;
mDrawPaint.setStrokeWidth(mBrushEraserSize);
mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
public void setBrushDrawingMode(boolean brushDrawMode) {
this.mBrushDrawMode = brushDrawMode;
if (brushDrawMode) {
this.setVisibility(View.VISIBLE);
refreshBrushDrawing();
}
}
public Bitmap getBrushBitmap() {
return brushBitmap;
}
public void setBrushBitmap(Bitmap brushBitmap) {
this.brushBitmap = brushBitmap;
}
public void setOpacity(#IntRange(from = 0, to = 255) int opacity) {
this.mOpacity = (int) (opacity * 2.55f);
setBrushDrawingMode(true);
}
public int getOpacity() {
return mOpacity;
}
boolean getBrushDrawingMode() {
return mBrushDrawMode;
}
public void setBrushSize(float size) {
mBrushSize = 5 + (int) (size);
setBrushDrawingMode(true);
}
void setBrushColor(#ColorInt int color) {
mDrawPaint.setColor(color);
setBrushDrawingMode(true);
}
void setBrushEraserSize(float brushEraserSize) {
this.mBrushEraserSize = brushEraserSize;
setBrushDrawingMode(true);
}
void setBrushEraserColor(#ColorInt int color) {
mDrawPaint.setColor(color);
setBrushDrawingMode(true);
}
float getEraserSize() {
return mBrushEraserSize;
}
public float getBrushSize() {
return mBrushSize;
}
int getBrushColor() {
return mDrawPaint.getColor();
}
public void clearAll() {
mDrawnPaths.clear();
mRedoPaths.clear();
if (mDrawCanvas != null) {
mDrawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
invalidate();
}
void setBrushViewChangeListener(BrushViewChangeListener brushViewChangeListener) {
mBrushViewChangeListener = brushViewChangeListener;
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Bitmap canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mDrawCanvas = new Canvas(canvasBitmap);
}
#Override
protected void onDraw(Canvas canvas) {
for (BrushLinePath linePath : mDrawnPaths) {
canvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
}
canvas.drawPath(mPath, mDrawPaint);
/////
final Bitmap scaledBitmap = getScaledBitmap();
final float centerX = scaledBitmap.getWidth() / 2;
final float centerY = scaledBitmap.getHeight() / 2;
final PathMeasure pathMeasure = new PathMeasure(mPath, false);
float distance = scaledBitmap.getWidth() / 2;
float[] position = new float[2];
float[] slope = new float[2];
float slopeDegree;
while (distance < pathMeasure.getLength())
{
pathMeasure.getPosTan(distance, position, slope);
slopeDegree = (float)((Math.atan2(slope[1], slope[0]) * 180f) / Math.PI);
canvas.save();
canvas.translate(position[0] - centerX, position[1] - centerY);
canvas.rotate(slopeDegree, centerX, centerY);
canvas.drawBitmap(scaledBitmap, 0, 0, mDrawPaint);
canvas.restore();
distance += scaledBitmap.getWidth() + 10;
}
}
/////
private Bitmap getScaledBitmap()
{
// width / height of the bitmap[
float width = brushBitmap.getWidth();
float height = brushBitmap.getHeight();
// ratio of the bitmap
float ratio = width / height;
// set the height of the bitmap to the width of the path (from the paint object).
float scaledHeight = mDrawPaint.getStrokeWidth();
// to maintain aspect ratio of the bitmap, use the height * ratio for the width.
float scaledWidth = scaledHeight * ratio;
// return the generated bitmap, scaled to the correct size.
return Bitmap.createScaledBitmap(brushBitmap, (int)scaledWidth, (int)scaledHeight, true);
}
/**
* Handle touch event to draw paint on canvas i.e brush drawing
*
* #param event points having touch info
* #return true if handling touch events
*/
#SuppressLint("ClickableViewAccessibility")
#Override
public boolean onTouchEvent(#NonNull MotionEvent event) {
if (mBrushDrawMode) {
float touchX = event.getX();
float touchY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchStart(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
touchMove(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
touchUp();
break;
}
invalidate();
return true;
} else {
return false;
}
}
boolean undo() {
if (!mDrawnPaths.empty()) {
mRedoPaths.push(mDrawnPaths.pop());
invalidate();
}
if (mBrushViewChangeListener != null) {
mBrushViewChangeListener.onViewRemoved(this);
}
return !mDrawnPaths.empty();
}
boolean redo() {
if (!mRedoPaths.empty()) {
mDrawnPaths.push(mRedoPaths.pop());
invalidate();
}
if (mBrushViewChangeListener != null) {
mBrushViewChangeListener.onViewAdd(this);
}
return !mRedoPaths.empty();
}
private void touchStart(float x, float y) {
mRedoPaths.clear();
mPath.reset();
mPath.moveTo(x, y);
mTouchX = x;
mTouchY = y;
if (mBrushViewChangeListener != null) {
mBrushViewChangeListener.onStartDrawing();
}
}
private void touchMove(float x, float y) {
float dx = Math.abs(x - mTouchX);
float dy = Math.abs(y - mTouchY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mTouchX, mTouchY, (x + mTouchX) / 2, (y + mTouchY) / 2);
mTouchX = x;
mTouchY = y;
}
}
private void touchUp() {
mPath.lineTo(mTouchX, mTouchY);
// Commit the path to our offscreen
mDrawCanvas.drawPath(mPath, mDrawPaint);
// kill this so we don't double draw
mDrawnPaths.push(new BrushLinePath(mPath, mDrawPaint));
/////
final Bitmap scaledBitmap = getScaledBitmap();
final float centerX = scaledBitmap.getWidth() / 2;
final float centerY = scaledBitmap.getHeight() / 2;
final PathMeasure pathMeasure = new PathMeasure(mPath, false);
float distance = scaledBitmap.getWidth() / 2;
float[] position = new float[2];
float[] slope = new float[2];
float slopeDegree;
while (distance < pathMeasure.getLength())
{
pathMeasure.getPosTan(distance, position, slope);
slopeDegree = (float)((Math.atan2(slope[1], slope[0]) * 180f) / Math.PI);
mDrawCanvas.save();
mDrawCanvas.translate(position[0] - centerX, position[1] - centerY);
mDrawCanvas.rotate(slopeDegree, centerX, centerY);
mDrawCanvas.drawBitmap(scaledBitmap, 0, 0, mDrawPaint);
mDrawCanvas.restore();
distance += scaledBitmap.getWidth() + 10;
}
/////
mPath = new Path();
if (mBrushViewChangeListener != null) {
mBrushViewChangeListener.onStopDrawing();
mBrushViewChangeListener.onViewAdd(this);
}
}
#VisibleForTesting
Paint getDrawingPaint() {
return mDrawPaint;
}
#VisibleForTesting
Pair<Stack<BrushLinePath>, Stack<BrushLinePath>> getDrawingPath() {
return new Pair<>(mDrawnPaths, mRedoPaths);
}
}
public interface BrushViewChangeListener {
void onViewAdd(BrushDrawingView brushDrawingView);
void onViewRemoved(BrushDrawingView brushDrawingView);
void onStartDrawing();
void onStopDrawing();
}
class BrushLinePath {
private final Paint mDrawPaint;
private final Path mDrawPath;
BrushLinePath(final Path drawPath, final Paint drawPaints) {
mDrawPaint = new Paint(drawPaints);
mDrawPath = new Path(drawPath);
}
Paint getDrawPaint() {
return mDrawPaint;
}
Path getDrawPath() {
return mDrawPath;
}
}
The reason it happens is because Paint doesn't have an alpha composing mode set by default. Thus, when you're trying to paint a bitmap over your canvas it will replace the destination pixels with your brush pixels, which in your case is #00000000. And that will result in pixel being displayed as black. Have a look into this documentation: https://developer.android.com/reference/android/graphics/PorterDuff.Mode
By the first glance it seems you're looking for PorterDuff.Mode.SRC_OVER or PorterDuff.Mode.SRC_ATOP - this way transparent pixels from your source image (your brush) will not over-draw the pixels from your destination (canvas). In case your background is always non-transparent, you will see no difference between SRC_OVER and SRC_ATOP, but if it isn't - choose the one which fits your needs. Then you can modify setupPathAndPaint method by adding this line to its end:
mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));

Detecting Fling touch in App

I am Trying to implement a swipe touch to control the paddle in the app, I've managed to get the paddle move by detecting a single touch but I can't understand how to make a fling work. I've tried implementing ths solution by referring the official android docs but this crashes the app. Any Advice?
package com.nblsoft.pong;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v4.view.GestureDetectorCompat;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
public class PongLogic extends View {
MainActivity mainactivity = (MainActivity) getContext();
private GestureDetectorCompat mDetector = new GestureDetectorCompat(mainactivity, new MyGestureListner());
//set screen constrains in dip
Configuration configuration = this.getResources().getConfiguration();
int dpHeight = configuration.screenHeightDp; //The current height of the available screen space, in dp units, corresponding to screen height resource qualifier.
int dpWidth = configuration.screenWidthDp; //The current width of the available screen space, in dp units, corresponding to screen width resource qualifier.
//int smallestScreenWidthDp = configuration.smallestScreenWidthDp; //The smallest screen size an application will see in normal operation, corresponding to smallest screen width resource qualifier.
//DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
//float dpHeight = displayMetrics.heightPixels / displayMetrics.density;
//float dpWidth = displayMetrics.widthPixels / displayMetrics.density;
private int dptopixel(int DESIRED_DP_VALUE) {
final float scale = getResources().getDisplayMetrics().density;
return (int) ((DESIRED_DP_VALUE) * scale + 0.5f);
}
private int pixeltodp(int DESIRED_PIXEL_VALUE) {
final float scale = getResources().getDisplayMetrics().density;
return (int) ((DESIRED_PIXEL_VALUE) - 0.5f / scale);
}
//set paddle size, speed, position vector
int AI_paddle_pos_x = 4 * (dptopixel(dpWidth) / 100); //3 for 320x480, 10 for 1080x1920 etc.
int paddle_width = (dptopixel(dpWidth) / 10); //
int AI_paddle_pos_y = (dptopixel(dpHeight) / 10); //48 for 320x480, 190 for 1080x1920 etc.
int paddle_height = (dptopixel(dpHeight) / 100) + 3; //the paddle is 100% of the total height of phone.
int user_paddle_pos_x = 4 * (dptopixel(dpWidth) / 100);
int user_paddle_pos_y = dptopixel(dpHeight) - ((dptopixel(dpHeight) / 10) + (dptopixel(dpHeight) / 100) + 3);
//Score
int score_user = 0;
int score_AI = 0;
//User Paddle
public Rect paddle_user = new Rect(user_paddle_pos_x,
user_paddle_pos_y,
user_paddle_pos_x + paddle_width,
user_paddle_pos_y + paddle_height);
int user_paddle_vel = 0;
//AI paddle
Rect paddle_AI = new Rect(AI_paddle_pos_x,
AI_paddle_pos_y,
AI_paddle_pos_x + paddle_width,
AI_paddle_pos_y + paddle_height);
//set ball position vector, Velocity vector, acceleration
int ball_pos_x = 0;
int ball_pos_y = (dptopixel(dpHeight) / 2);
int ball_size = dptopixel(dpWidth) / 100;
int ball_velocity_x = 1;
int ball_velocity_y = 3;
// Ball
Rect ball = new Rect(ball_pos_x,
ball_pos_y,
ball_pos_x + ball_size,
ball_pos_y + ball_size);
//Override onDraw method
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint myUI = new Paint();
myUI.setColor(Color.WHITE);
Paint myscore = new Paint();
myscore.setTextSize(dptopixel(dpWidth / 20));
myscore.setColor(Color.WHITE);
//mytext.setStyle(Paint.Style.STROKE);
//mytext.setStrokeWidth(2);
// Draw Score
canvas.drawText(Integer.toString(score_AI), (dptopixel(dpWidth) / 10), (dptopixel(dpHeight) / 4), myscore);
canvas.drawText(Integer.toString(score_user), (dptopixel(dpWidth) / 10), 3 * (dptopixel(dpHeight) / 4), myscore);
// Draw Middle point
canvas.drawRect(0, ((dptopixel(dpHeight)) / 2), (dptopixel(dpWidth)), (((dptopixel(dpHeight)) / 2) + 2), myUI);
// Draw both paddles
canvas.drawRect(paddle_user, myUI);
canvas.drawRect(paddle_AI, myUI);
// Draw ball
canvas.drawRect(ball, myUI);
//Practise Methods
//canvas.drawText(Integer.toString(dptopixel(dpHeight)),300,300,mytext);
//canvas.drawText(Integer.toString(dptopixel(dpWidth)), 400, 400, mytext);
//canvas.drawText(Integer.toString(dpHeight),500,500,mytext);
//canvas.drawText(Integer.toString(dpWidth),600,600,mytext);
//canvas.drawText("Fuck", 700, 700, mytext);
//canvas.drawRect(0,0,dptopixel(dpWidth),dptopixel(dpHeight),mytext);
//Game Loop Updater
update();
invalidate();
}
private void update() {
if(ball.centerY() < dptopixel(dpHeight)/2){ paddle_AI.offsetTo(ball.centerX()- dptopixel(10), AI_paddle_pos_y); }
if (paddle_user.contains(ball)) {
ball_velocity_y = ball_velocity_y * -1;
} else if (paddle_AI.contains(ball)) {
ball_velocity_y = ball_velocity_y * -1;
} else if ((ball.centerX() > (dptopixel(dpWidth))) || (ball.centerX() < 0)) {
ball_velocity_x = ball_velocity_x * -1;
} else if (ball.centerY() < 0) {
//Update the user score
score_user = 1 + score_user;
//re draw the ball
ball.offsetTo(0, (dptopixel(dpHeight) / 2));
} else if (ball.centerY() > (dptopixel(dpHeight))) {
//Update the AI score
score_AI = 1 + score_AI;
//re draw the ball
ball.offsetTo(0, (dptopixel(dpHeight) / 2));
}
ball.offset(ball_velocity_x, ball_velocity_y);
}
//Override Touch method
/*
//
NOT WORKING SWIPE METHODS
float x1, x2, y1, y2;
final int MIN_DISTANCE = 70;
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
y1 = event.getY();
break;
case MotionEvent.ACTION_UP:
x2 = event.getX();
y2 = event.getY();
float deltaX = x2 - x1;
float deltaY = y2 - y1;
if (deltaX > MIN_DISTANCE)
{
paddle_user.offset(1, 0);
break;
}
else if (Math.abs(deltaX) > MIN_DISTANCE)
{
paddle_user.offset(-1, 0);
break;
}
}
return true;
}*/
#Override
public boolean onTouchEvent(MotionEvent event) {
this.mDetector.onTouchEvent(event);
return super.onTouchEvent(event);
/*
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (event.getX() > dptopixel(dpWidth) / 2) {
paddle_user.offset(10, 0);
} else {
paddle_user.offset(-10, 0);
}
}
return true;
*/
}
/* #Override
public boolean onTouch(View v, MotionEvent event) {
this.paddle_user.offsetTo(10,10);
return true; //Event Handled
}
*/
public PongLogic(Context context) {
super(context);
setBackgroundColor(Color.BLACK); //to set background
this.setFocusableInTouchMode(true); //to enable touch mode
}
class MyGestureListner extends GestureDetector.SimpleOnGestureListener{
#Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
if (event1.getAction() == MotionEvent.ACTION_DOWN) {
user_paddle_vel = (int) velocityX;
paddle_user.offset(user_paddle_vel,0);
}
else if(event1.getAction() == MotionEvent.ACTION_UP){
user_paddle_vel = 0;
}
return true;
}
}
}
You can take a look to my article in Medium, I explained step by step how to do it: https://medium.com/#euryperez/android-pearls-detect-swipe-and-touch-over-a-view-203ae2c028dc#.lcmg4jytc

rotate dial in limited degrees

All I want rotate image in particular angle as like below image. I have code for rotation but it rotate 360 degree but I want it only for particular degrees and get the selected number which is upper side of dial.
below is my code.
My custom View this work fine but lake of perfomance.
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
public class MyDialView extends View implements OnGestureListener{
private static Bitmap bimmap;
private static Paint paint;
private static Rect bounds;
private int totalNicks = 100;
private int currentNick = 0;
private GestureDetector gestureDetector;
private float dragStartDeg = Float.NaN;
float dialerWidth = 0,dialerHeight = 0;
private static Paint createDefaultPaint() {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
return paint;
}
private float xyToDegrees(float x, float y) {
float distanceFromCenter = PointF.length((x - 0.5f), (y - 0.5f));
if (distanceFromCenter < 0.1f
|| distanceFromCenter > 0.5f) { // ignore center and out of bounds events
return Float.NaN;
} else {
return (float) Math.toDegrees(Math.atan2(x - 0.5f, y - 0.5f));
}
}
public final float getRotationInDegrees() {
return (360.0f / totalNicks) * currentNick;
}
public final void rotate(int nicks) {
currentNick = (currentNick + nicks);
if (currentNick >= totalNicks) {
currentNick %= totalNicks;
} else if (currentNick < 0) {
currentNick = (totalNicks + currentNick);
}
Log.e("Current nick", String.valueOf(currentNick));
if((currentNick > 80 || currentNick < 20)){
invalidate();
}
}
public MyDialView(Context context, AttributeSet attrs) {
super(context, attrs);
bimmap = BitmapFactory.decodeResource(context.getResources(),R.drawable.out_round);
paint = createDefaultPaint();
gestureDetector = new GestureDetector(getContext(), this);
dialerWidth = bimmap.getWidth() /2.0f;
dialerHeight = bimmap.getHeight() / 2.0f;
bounds = new Rect();
}
#Override
protected void onDraw(Canvas canvas) {
canvas.getClipBounds(bounds);
canvas.save(Canvas.MATRIX_SAVE_FLAG);
//{
canvas.translate(bounds.left, bounds.top);
float rotation = getRotationInDegrees();
canvas.rotate(rotation, dialerWidth, dialerHeight);
canvas.drawBitmap(bimmap, 0,0,null);
//canvas.rotate(- rotation, dialerWidth, dialerHeight);
//}
canvas.restore();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
return true;
} else {
return super.onTouchEvent(event);
}
}
//Gesture detector methods
#Override
public boolean onDown(MotionEvent e) {
float x = e.getX() / ((float) getWidth());
float y = e.getY() / ((float) getHeight());
dragStartDeg = xyToDegrees(x, y);
//Log.d("deg = " , ""+dragStartDeg);
if (! Float.isNaN(dragStartDeg)) {
return true;
} else {
return false;
}
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return false;
}
#Override
public void onLongPress(MotionEvent e) {
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
if (! Float.isNaN(dragStartDeg)) {
float currentDeg = xyToDegrees(e2.getX() / getWidth(),
e2.getY() / getHeight());
if (! Float.isNaN(currentDeg)) {
float degPerNick = 360.0f / totalNicks;
float deltaDeg = dragStartDeg - currentDeg;
final int nicks = (int) (Math.signum(deltaDeg)
* Math.floor(Math.abs(deltaDeg) / degPerNick));
if (nicks != 0) {
dragStartDeg = currentDeg;
rotate(nicks);
}
}
return true;
} else {
return false;
}
}
#Override
public void onShowPress(MotionEvent e) {
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
}
I want 0-9 according to user selection & also allow user rotation to 0-9 not more rotation.
I have also check another code this is below.
dialer = (ImageView) findViewById(R.id.imageView_ring);
dialer.setOnTouchListener(new MyOnTouchListener());
dialer.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// method called more than once, but the values only need to be initialized one time
if (dialerHeight == 0 || dialerWidth == 0) {
dialerHeight = dialer.getHeight();
dialerWidth = dialer.getWidth();
// resize
Matrix resize = new Matrix();
resize.postScale((float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getWidth(), (float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getHeight());
imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false);
// translate to the image view's center
float translateX = dialerWidth / 2 - imageScaled.getWidth() / 2;
float translateY = dialerHeight / 2 - imageScaled.getHeight() / 2;
matrix.postTranslate(translateX, translateY);
dialer.setImageBitmap(imageScaled);
dialer.setImageMatrix(matrix);
Log.e("Rotation degree :"+rotationDegrees, String.valueOf(tickNumber));
}
}
});
int tickNumber = 0;
private void rotateDialer(float degrees) {
//System.out.println("Rotation Done :: "+rotationDone);
// if(!rotationDone) {
this.rotationDegrees += degrees;
this.rotationDegrees = this.rotationDegrees % 360;
tickNumber = (int)this.rotationDegrees*100/360;
// It could be negative
if (tickNumber > 0) tickNumber = 100 - tickNumber;
//this.rotationDegrees = Math.abs(rotationDegrees);
this.tickNumber = Math.abs(tickNumber);
if(tickNumber < 20 || tickNumber > 80){
Log.e("Rotation degree :"+rotationDegrees, String.valueOf(tickNumber));
matrix.postRotate(degrees, dialerWidth / 2, dialerHeight / 2);
dialer.setImageMatrix(matrix);
}
// }
}
/**
* #return The angle of the unit circle with the image view's center
*/
private double getAngle(double xTouch, double yTouch) {
double delta_x = xTouch - (dialerWidth) /2;
double delta_y = (dialerHeight) /2 - yTouch;
double radians = Math.atan2(delta_y, delta_x);
double dx = xTouch - dWidth;
double dy = (dHeight - ((dialerHeight) /2)) - yTouch;
double dRadi = Math.atan2(dy, dx);
//Log.e("MY degree", String.valueOf( Math.toDegrees(dRadi)));
//return Math.toDegrees(dRadi);
return Math.toDegrees(radians);
}
/**
* Simple implementation of an {#link OnTouchListener} for registering the dialer's touch events.
*/
private class MyOnTouchListener implements OnTouchListener {
private double startAngle;
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// reset the touched quadrants
/*for (int i = 0; i < quadrantTouched.length; i++) {
quadrantTouched[i] = false;
}*/
//allowRotating = false;
startAngle = getAngle(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
/*double rotationAngleRadians = Math.atan2(event.getX() - (dialer.getWidth() / 2 ), ( (dialer.getHeight() / 2 ) - event.getY()));
double angle = (int) Math.toDegrees(rotationAngleRadians);
Log.i("gg", "rotaion angle"+angle);*/
double currentAngle = getAngle(event.getX(), event.getY());
//if(currentAngle < 130 || currentAngle < 110){
//Log.e("Start angle :"+startAngle, "Current angle:"+currentAngle);
rotateDialer((float) (startAngle - currentAngle));
startAngle = currentAngle;
//}
//Log.e("MOVE start Degree:"+startAngle, "Current Degree :"+currentAngle);
break;
case MotionEvent.ACTION_UP:
//allowRotating = true;
break;
}
// set the touched quadrant to true
//quadrantTouched[getQuadrant(event.getX() - (dialerWidth / 2), dialerHeight - event.getY() - (dialerHeight / 2))] = true;
//detector.onTouchEvent(event);
return true;
}
}
I do not understand your problem. The code below rotate the image 48 degrees.
ImageView dialer = (ImageView) findViewById(R.id.imageView_ring);
int degrees = 48;
Matrix matrix = new Matrix();
matrix.setRotate(degrees);
Bitmap bmpBowRotated = Bitmap.createBitmap(imageOrginal, 0, 0, imageOrginal.getWidth(),imageOrginal.getHeight(), matrix, false);
dialer.setImageBitmap(bmpBowRotated);
Hi Girish there is a class Named RotateAnimation by using this class u can easily do it
look Example like
RotateAnimation r = new RotateAnimation(0f, -90f,200,200); // HERE
r.setStartOffset(1000);
r.setDuration(1000);
r.setFillAfter(true); //HERE
animationSet.addAnimation(r);
I would like to first know what will be there for deployment? Does it allow manipulation Evenets? if yes then you get handle ManipulationStatring and ManipulationDelta Event to rotate the element.
If the same is not the case where Manipulation is not available then you can try RenderTransformation property with RorateTransform of the element is you are working with WPf.
I was able to achieve this by doing few of the following tweaks on your code
Making user click exactly on the arrow always to get the initial angle at which the arrow is placed, in your case 90 degree, else return false
Also save the angle at which the user removed his finger and use that angle as the initial value for his next touch ,like if he placed arrow at 100 deg make that his initial touch position to activate rotation again
Now for checking his answer take the angle at which your numbers 0 to 9 are placed ,im guessing your values take 120 deg from 0 to 9, divide that angle by 10, you can easily find out what angle represents what value and get your result
Also touching exactly at 90deg to begin rotation is very irritating, so always check for value which is bw 90+4 and 90-4 to begin, but always use the 90 as your start angle

Rotated OpenStreetMap view - how to Swipe map in direction of finger move after rotation in Android?

I am using the OSM for a mapping application where I
rotate the map in direction of travel as explained here Android Rotating MapView . This works well.
However, I haven't yet managed to adjust the dispatchTouchEvent code
to counter the map rotation effect for the user touches (right now
when the map is rotated 90 degrees a user's horizontal sweep will move
the map vertically etc). The sample code only offers the teaser:
public boolean dispatchTouchEvent(MotionEvent ev) {
// TODO: rotate events too
return super.dispatchTouchEvent(ev);
}
Any guidance would be appreciated.
And while I am at it - Is it still possible to position the zoom
controls separately, so that they do NOT rotate when the map rotates?
I read that the getZoomControls() is deprecated. (Why ?)
I know it's too late but it may help someone...
import org.osmdroid.views.overlay.MyLocationOverlay;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Canvas;
import android.graphics.Matrix ;
import android.hardware.Sensor;
import android.os.BatteryManager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
/**
* Rotate the Map in accordance to the movement of user and always point to North
*
*/
public class RotatingRelativeLayout extends RelativeLayout
{
private Matrix mMatrix = new Matrix();
private float[] mTemp = new float[2];
private Context context;
private static final float SQ2 = 1.414213562373095f;
public RotatingRelativeLayout(final Context pContext,
final AttributeSet pAttrs)
{
super(pContext, pAttrs);
this.context=pContext;
}
#Override
protected void dispatchDraw(Canvas canvas) {
long rotateTime = MyLocationOverlay.getTimeOfMovement();
float overlayBearing = MyLocationOverlay.getBearing();//this method returns current bearing from OSM
long currentTime = System.currentTimeMillis();
long diffTime = currentTime-rotateTime;
/*
* Here we rotate map in accordance with Compass to point always North
*
*/
if(diffTime >= (40*1000 )){
//isBearing=false;
overlayBearing=0;
canvas.rotate(overlayBearing, getWidth() * 0.5f, getHeight() * 0.5f);
}
else
/*
* Rotate Map According to the user movement
*/
canvas.rotate(-overlayBearing, getWidth() * 0.5f, getHeight() * 0.5f);
canvas.getMatrix().invert(mMatrix);
final float w = this.getWidth();
final float h = this.getHeight();
final float scaleFactor = (float)(Math.sqrt(h * h + w * w) / Math.min(w, h));
canvas.scale(scaleFactor, scaleFactor, getWidth() * 0.5f, getHeight() * 0.5f);
super.dispatchDraw(canvas);
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.restore();
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = getWidth();
final int height = getHeight();
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View view = getChildAt(i);
final int childWidth = view.getMeasuredWidth();
final int childHeight = view.getMeasuredHeight();
final int childLeft = (width - childWidth) / 2;
final int childTop = (height - childHeight) / 2;
view.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int h = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
int sizeSpec;
if (w > h) {
sizeSpec = MeasureSpec.makeMeasureSpec((int) (w*SQ2), MeasureSpec.EXACTLY);
}
else {
sizeSpec = MeasureSpec.makeMeasureSpec((int) (h*SQ2), MeasureSpec.EXACTLY);
}
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).measure(sizeSpec, sizeSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
final float[] temp = mTemp;
temp[0] = event.getX();
temp[1] = event.getY();
mMatrix.mapPoints(temp);
event.setLocation(temp[0], temp[1]);
return super.dispatchTouchEvent(event);
}
}
now just use this Relative Layout in your xml like this:-
<YourPackageName.RotatingRelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="#+id/rotating_layout" android:layout_marginBottom="40dip"/>
And add Map View Programatially like this:-
// Find target container
final RelativeLayout rl=(RelativeLayout)mParent.findViewById(R.id.rotating_layout);
// Create rotator
mRotator=new RotatingRelativeLayout(mParent, null);
// Add map to the rotating layout
// Add rotator to the screen
rl.addView(mMap,new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
here mParent is Context. And one more thing if you encounter Image Pixelation prob you just have to use it
//Paint distortion handling..
p.setFilterBitmap(true);
Hope i explained it as good as i could.... feel free to ask if you find problem understanding it.
Thanks.

How to implement the ScrollImageView class in my Android application

I found this class:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
// borrowed from https://sites.google.com/site/androidhowto/how-to-1/custom-scrollable-image-view
public class ScrollImageView extends View {
private final int DEFAULT_PADDING = 10;
private Display mDisplay;
private Bitmap mImage;
/* Current x and y of the touch */
private float mCurrentX = 0;
private float mCurrentY = 0;
private float mTotalX = 0;
private float mTotalY = 0;
/* The touch distance change from the current touch */
private float mDeltaX = 0;
private float mDeltaY = 0;
int mDisplayWidth;
int mDisplayHeight;
int mPadding;
public ScrollImageView(Context context) {
super(context);
initScrollImageView(context);
}
public ScrollImageView(Context context, AttributeSet attributeSet) {
super(context);
initScrollImageView(context);
}
private void initScrollImageView(Context context) {
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mPadding = DEFAULT_PADDING;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = measureDim(widthMeasureSpec, mDisplay.getWidth());
int height = measureDim(heightMeasureSpec, mDisplay.getHeight());
setMeasuredDimension(width, height);
}
private int measureDim(int measureSpec, int size) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = size;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
public Bitmap getImage() {
return mImage;
}
public void setImage(Bitmap image) {
mImage = image;
}
public int getPadding() {
return mPadding;
}
public void setPadding(int padding) {
this.mPadding = padding;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mCurrentX = event.getRawX();
mCurrentY = event.getRawY();
}
else if (event.getAction() == MotionEvent.ACTION_MOVE) {
float x = event.getRawX();
float y = event.getRawY();
// Update how much the touch moved
mDeltaX = x - mCurrentX;
mDeltaY = y - mCurrentY;
mCurrentX = x;
mCurrentY = y;
invalidate();
}
// Consume event
return true;
}
#Override
protected void onDraw(Canvas canvas) {
if (mImage == null) {
return;
}
float newTotalX = mTotalX + mDeltaX;
// Don't scroll off the left or right edges of the bitmap.
if (mPadding > newTotalX && newTotalX > getMeasuredWidth() - mImage.getWidth() - mPadding)
mTotalX += mDeltaX;
float newTotalY = mTotalY + mDeltaY;
// Don't scroll off the top or bottom edges of the bitmap.
if (mPadding > newTotalY && newTotalY > getMeasuredHeight() - mImage.getHeight() - mPadding)
mTotalY += mDeltaY;
Paint paint = new Paint();
canvas.drawBitmap(mImage, mTotalX, mTotalY, paint);
}
}
and am stumped how to use it with my application.
I want my application when in landscape orientation to display a bitmap image that is scrollable vertically and horizontally. I pull my image from a URL and programatically make it a bitmap. I call a method that returns my bitmap image.
Any ideas?
The setImage methods takes a Bitmap. So, you should be able to programmatically create this view in your activity, set the image with your Bitmap and place the view into your layout.
I've never seen this class before, so your mileage may vary ;)

Categories

Resources