Loop animation once in LibGDX - android

I have an explosion animation i need to loop once and then dissapear, but is not working,
This part its supposed to do the trick: birdAnimationHit.setPlayMode(Animation.NORMAL);
But it only shows the last image of the loop. Don't know what is wrong, here is my code
AssetLoader.java
public static void load() {
texture = new Texture(Gdx.files.internal("data/texture.png"));
texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
dustHit1 = new TextureRegion(texture, 930, 154, 140, 158);
dustHit1.flip(false, true);
dustHit2 = new TextureRegion(texture, 1079, 154, 187, 158);
dustHit2.flip(false, true);
dustHit3 = new TextureRegion(texture, 1274, 154, 149, 158);
dustHit3.flip(false, true);
dustHit4 = new TextureRegion(texture, 1430, 154, 153, 158);
dustHit4.flip(false, true);
dustHit5 = new TextureRegion(texture, 1590, 154, 155, 158);
dustHit5.flip(false, true);
TextureRegion[] birdsHit = { dustHit1, dustHit2, dustHit3, dustHit4, dustHit5 };
birdAnimationHit = new Animation(0.06f, birdsHit);
birdAnimationHit.setPlayMode(Animation.NORMAL);
}
GameRenderer.java
private void drawBirdHit(float runTime) {
batcher.draw(birdAnimationHit.getKeyFrame(runTime), bird.getX(),
bird.getY(), bird.getWidth() / 2.0f,
bird.getHeight() / 2.0f, bird.getWidth(), bird.getHeight(),
1.4f, 1.4f, bird.getRotation());
}

You need to sum up the runTime inside of that logic. Else you always get the same frame. Take a look at the getKeyFrame method: (Marked the importend part for you)
public TextureRegion getKeyFrame (float stateTime) {
int frameNumber = getKeyFrameIndex(stateTime);
return keyFrames[frameNumber];
}
public int getKeyFrameIndex (float stateTime) {
if (keyFrames.length == 1) return 0;
int frameNumber = (int)(stateTime / frameDuration); //this will always produce the same picture if you dont sum up the time!
switch (playMode) {
case NORMAL:
frameNumber = Math.min(keyFrames.length - 1, frameNumber);
break;
case LOOP:
frameNumber = frameNumber % keyFrames.length;
break;
case LOOP_PINGPONG:
frameNumber = frameNumber % ((keyFrames.length * 2) - 2);
if (frameNumber >= keyFrames.length) frameNumber = keyFrames.length - 2 - (frameNumber - keyFrames.length);
break;
case LOOP_RANDOM:
frameNumber = MathUtils.random(keyFrames.length - 1);
break;
case REVERSED:
frameNumber = Math.max(keyFrames.length - frameNumber - 1, 0);
break;
case LOOP_REVERSED:
frameNumber = frameNumber % keyFrames.length;
frameNumber = keyFrames.length - frameNumber - 1;
break;
}
return frameNumber;
}
So hold somewhere a timer where you sumup the deltatimes
and change your code to something like this:
private void drawBirdHit(float runTime) {
sum += runTime;
batcher.draw(birdAnimationHit.getKeyFrame(sum), bird.getX(),
bird.getY(), bird.getWidth() / 2.0f,
bird.getHeight() / 2.0f, bird.getWidth(), bird.getHeight(),
1.4f, 1.4f, bird.getRotation());
}
The Animation itself does not have a timer for that. You can just request the right keyframe depending on the past time, not on the current deltatime.

What I do is simply include a blank frame as the last frame of any animation; you only need one per sprite sheet that can be used for multiple animations. Then just call the stop/play methods, the animation will run then disappear, ready for the next play on collision or whatever.

Related

Attaching a sprite to a Box2d body for movement

Im new on Box2d got a problem and couldnt solve it.
I want to move my player left and right when the user touch my left and right buttons.
I created a fixture I can move body and fixture but not the player sprite
How can I attach my player sprite to my body ?
and How should I control body because I cant stop it.
I want to find a proper way of controlling player in box2d. I couldnt use setLinerVelocity etc.
this is my codes
public World world;
public Body bplayer;
public Box2DDebugRenderer b2dr;
public Matrix4 cameraBox2D;
PlayScreen
buttonimage.addListener(new ClickListener() {
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button)
{
bplayer.setLinearVelocity(-5*PPM , 0);
return true;
}
});
world = new World(new Vector2(player.getPosition().x , player.getPosition().y) , false);
b2dr = new Box2DDebugRenderer();
bplayer = createPlayer(player.getPosition().x , player.getPosition().y);
show method
buttonimage.setPosition(160,0);
rightbuttonimage.setPosition(320,0);
pauseimage.setPosition(220,-20);
cameraBox2D = camera.combined.cpy();
Render method
Gdx.gl.glClearColor(0, 0, 2f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
sb.setProjectionMatrix(camera.combined);
player.position.y += 500 * Gdx.graphics.getDeltaTime();
sb.begin();
sb.draw(bg, 0, camera.position.y - (camera.viewportHeight/2));
sb.draw(player.sprite, player.getPosition().x , player.getPosition().y);
for (Tube tube : tubes) {
sb.draw(tube.getlefttube(), tube.getposlefttube().x, tube.getposlefttube().y);
sb.draw(tube.getrighttube(), tube.getposrighttube().x, tube.getposrighttube().y);
sb.draw(tube.getLight() , tube.getPoslight().x , tube.getPoslight().y);
}
delta*=speed;
sb.end();
update(delta);
b2dr.render(world , cameraBox2D);
stage.draw();
app.batch.begin();
app.font23.draw(app.batch,"Lights collected :" + dropsGathered , 0, 720);
app.batch.end();
cameraUpdate method
Vector3 position = camera.position;
position.x = player.position.x;
position.y = player.position.y;
camera.position.set(position);
createPlayer method
Body pBody;
BodyDef def = new BodyDef();
def.type = BodyDef.BodyType.DynamicBody;
def.position.set(x * PPM, y * PPM );
def.fixedRotation = true;
pBody = world.createBody(def);
return pBody;
update method
world.step(1 / 60f , 6 , 2);
for(int i = 0; i < tubes.size; i++) {
Tube tube = tubes.get(i);
if (camera.position.y - (camera.viewportWidth/2) > tube.getposlefttube().y + tube.getlefttube().getWidth()) {
tube.reposition(tube.getposlefttube().y + ( TUBE_COUNT) );
}
if (tube.collides(player.getBounds())){
app.setScreen(new GameOver(app));
}
if (tube.gathered(player.getBounds())){
dropsGathered++;
}
if (dropsGathered >= 50){
//app.setScreen(new Stage2(app));
}
}
camera.update();
handleInput();
camera.position.y = player.getPosition().y + 300;
player.update(delta);
camera.update();
cameraUpdate(delta);
stage.act(delta);
Do not use the Sprite class. Use the TextureRegion class instead. Sprite is confusingly subclassed from TextureRegion, so when you call batch.draw(sprite, ...) its position and rotation parameters are ignored because it is being treated as a TextureRegion.
You could use a Sprite by calling sprite.draw(batch) but a Sprite is redundant because your Body already has position and rotation parameters.
Use a TextureRegion directly with the SpriteBatch. You can orient it with rotation parameters passed into the draw method.

Running OpenCV eye detection from within Android service

I want to run eye detection by OpenCV4Android from Android background service. I have a piece of code that runs well but as an Activity not service. I understand that the Android camera must have a preview to open. So I have created a preview (small one to make it looks hidden, since I want the processing to be in the background) and started the camera for recording. The camera starts successfully, but OpenCV doesn't detect eyes and faces. It only loads the xml classifiers. I expected the callbacks of OpenCV like onCameraViewStarted and onCameraFrame to get called when I open the camera for recording, but they didn't.
Here is the code:
public class BackgroundService extends Service implements SurfaceHolder.Callback, CameraBridgeViewBase.CvCameraViewListener2 {
private static final String TAG = "OCVSample::Activity";
private static final Scalar FACE_RECT_COLOR = new Scalar(0, 255, 0, 255);
public static final int JAVA_DETECTOR = 0;
private static final int TM_SQDIFF = 0;
private static final int TM_SQDIFF_NORMED = 1;
private static final int TM_CCOEFF = 2;
private static final int TM_CCOEFF_NORMED = 3;
private static final int TM_CCORR = 4;
private static final int TM_CCORR_NORMED = 5;
private int learn_frames = 0;
private Mat templateR;//right eye template
private Mat templateL; // left eye template
int method = 0;
private MenuItem mItemFace50;
private MenuItem mItemFace40;
private MenuItem mItemFace30;
private MenuItem mItemFace20;
private MenuItem mItemType;
private Mat mRgba;
private Mat mGray;
// matrix for zooming
private Mat mZoomWindow;
private Mat mZoomWindow2;
private File mCascadeFile;
private CascadeClassifier mJavaDetector;
private CascadeClassifier mJavaDetectorEye;
private int mDetectorType = JAVA_DETECTOR;
private String[] mDetectorName;
private float mRelativeFaceSize = 0.2f;
private int mAbsoluteFaceSize = 0;
private CameraBridgeViewBase mOpenCvCameraView;
private SeekBar mMethodSeekbar;
private TextView mValue;
double xCenter = -1;
double yCenter = -1;
MediaRecorder mediaRecorder;
// Binder given to clients
private final IBinder mBinder = new LocalBinder();
public class LocalBinder extends Binder {
BackgroundService getService() {
// Return this instance of this service so clients can call public methods
return BackgroundService.this;
}
}//end inner class that returns an instance of the service.
#Override
public IBinder onBind(Intent intent) {
return mBinder;
}//end onBind.
private WindowManager windowManager;
private SurfaceView surfaceView;
private Camera camera = null;
#Override
public void onCreate() {
// Start foreground service to avoid unexpected kill
Notification notification = new Notification.Builder(this)
.setContentTitle("Background Video Recorder")
.setContentText("")
.setSmallIcon(R.drawable.vecsat_logo)
.build();
startForeground(1234, notification);
// Create new SurfaceView, set its size to 1x1, move it to the top left corner and set this service as a callback
windowManager = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
surfaceView = new SurfaceView(this);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
100, 100,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT
);
Log.i(TAG, "100 x 100 executed");
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
windowManager.addView(surfaceView, layoutParams);
surfaceView.getHolder().addCallback(this);
//constructor:
mDetectorName = new String[2];// contains 3 positions..
mDetectorName[JAVA_DETECTOR] = "Java"; //let the detector be of type java detector, specify that in the JAVA_DETECTOR index.
Log.i(TAG, "Instantiated new " + ((Object) this).getClass().getSimpleName());
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_11, this,
mLoaderCallback); //once the application is resumed reload the library.
}
// Method called right after Surface created (initializing and starting MediaRecorder)
#Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
Log.i(TAG, "surfaceCreated method");
camera = Camera.open(1);
camera.unlock();
mediaRecorder = new MediaRecorder();
mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface());
mediaRecorder.setCamera(camera);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
mediaRecorder.setOutputFile(
Environment.getExternalStorageDirectory()+"/"+
DateFormat.format("yyyy-MM-dd_kk-mm-ss", new Date().getTime())+
".mp4"
);
try { mediaRecorder.prepare(); } catch (Exception e) {}
mediaRecorder.start();
}
// Stop recording and remove SurfaceView
#Override
public void onDestroy() {
Log.i(TAG, "surfaceDestroyed method");
camera.lock();
camera.release();
windowManager.removeView(surfaceView);
}
#Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {}
#Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
#Override
public void onManagerConnected(int status) {
//int status, status of initialization, sucess or not..
//now make a switch for the status cases: under success case do the work, load the classifiers..
switch (status) {
case LoaderCallbackInterface.SUCCESS: {
Log.i(TAG, "OpenCV loaded successfully"); // was loaded and initialized successfully..
try {
// load cascade file from application resources
InputStream is = getResources().openRawResource(
R.raw.lbpcascade_frontalface); // get the face classifier from the resource.
File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
mCascadeFile = new File(cascadeDir,
"lbpcascade_frontalface.xml"); // create a directory inside your app, and a file inside it to store the
FileOutputStream os = new FileOutputStream(mCascadeFile); // prepare an output stream that will write the classifier's code on the file in the app.
//read and write
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
is.close();
os.close();
// --------------------------------- load left eye
// classificator -----------------------------------
InputStream iser = getResources().openRawResource(
R.raw.haarcascade_lefteye_2splits);
File cascadeDirER = getDir("cascadeER",
Context.MODE_PRIVATE);
File cascadeFileER = new File(cascadeDirER,
"haarcascade_eye_right.xml");
FileOutputStream oser = new FileOutputStream(cascadeFileER);
byte[] bufferER = new byte[4096];
int bytesReadER;
while ((bytesReadER = iser.read(bufferER)) != -1) {
oser.write(bufferER, 0, bytesReadER);
}
iser.close();
oser.close();
//check if you can load the classifer.
mJavaDetector = new CascadeClassifier(
mCascadeFile.getAbsolutePath());
if (mJavaDetector.empty()) {
Toast.makeText(getApplicationContext(), "face classifier error", Toast.LENGTH_LONG).show();
Log.e(TAG, "Failed to load cascade face classifier");
mJavaDetector = null;
} else
Log.i(TAG, "Loaded cascade classifier from "
+ mCascadeFile.getAbsolutePath());
mJavaDetectorEye = new CascadeClassifier(
cascadeFileER.getAbsolutePath());
if (mJavaDetectorEye.empty()) {
Toast.makeText(getApplicationContext(), "eye classifer error", Toast.LENGTH_LONG).show();
Log.e(TAG, "Failed to load cascade eye classifier");
mJavaDetectorEye = null;
} else
Log.i(TAG, "Loaded cascade classifier from "
+ mCascadeFile.getAbsolutePath());
cascadeDir.delete();
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Failed to load cascade. Exception thrown: " + e);
}
//Whether classifiers are opened or not, open the front camera.
// mOpenCvCameraView.setCameraIndex(1);
//mOpenCvCameraView.enableFpsMeter(); // What is this? This method enables label with fps value on the screen
// mOpenCvCameraView.enableView(); // What? This means enable connecting to the camera.
}
break;
default: {
//When the loading of the libarary is failed
super.onManagerConnected(status);
}
break;
}
}
}; // end the class.
public void onCameraViewStarted(int width, int height) {
Log.i(TAG, "onCameraViewStarted method");
//onCameraViewStarted callback will be delivered only after enableView is called and surface is available
//This method is a member of CvCameraViewListener2, and we must implement it.
mGray = new Mat(); //initialize new gray scale matrix to contain the img pixels.
mRgba = new Mat(); //initialize new rgb matrix to contain the img pixels.
}
public void onCameraViewStopped() {
Log.i(TAG, "onCameraViewStopped method");
//Release the allocated memory
//release the matrix, this releases the allocated space in memory, since mat contains a header that contains img info and a pointer that points to the matrix in the memory.
mGray.release();
mRgba.release();
mZoomWindow.release();
mZoomWindow2.release();
}
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
Log.i(TAG, "onCameraFrame method");
//This method is a member of CvCameraViewListener2, and we must implement it.
// In this method we get every frame from the camera and process it in order to track the objects.
//inputFrame is the received frame from the camera.
mRgba = inputFrame.rgba(); //convert the frame to rgba scale, then assign this value to the rgba Mat img matrix.
mGray = inputFrame.gray(); //convert the frame to gray scale, then assign this value to the gray Mat img matrix.
//Shall we consider Flipping the camera img horizontally.
if (mAbsoluteFaceSize == 0) {
int height = mGray.rows(); //get the height of the captured frame stored in mgray Mat array (rows), why gray to rgb???
if (Math.round(height * mRelativeFaceSize) > 0) { //multiply that height with 0.2... Is the result > 0?
//if yes this indicates that there is a frame that was captured (it's height is not zero), so set the face size to
// Math.round(height * mRelativeFaceSize)
mAbsoluteFaceSize = Math.round(height * mRelativeFaceSize);
}
}
if (mZoomWindow == null || mZoomWindow2 == null)
CreateAuxiliaryMats();
MatOfRect faces = new MatOfRect(); //a matrix that will contain rectangles around the face (including the faces inside the rectangles), it will be filled by detectMultiScale method.
//if mJavaDetector is not null, this contains the face classifier that we have loaded previously
if (mJavaDetector != null)
//if not null, use this classifier to detect faces.
mJavaDetector.detectMultiScale(mGray, faces, 1.1, 2,
2, // TODO: objdetect.CV_HAAR_SCALE_IMAGE
new Size(mAbsoluteFaceSize, mAbsoluteFaceSize),
new Size());
//in th function detectMultiScale above,
// faces is the array that will contain the rectangles around the detected face.
// the 3rd param: specifies how much the image size is reduced at each image scale.
//4th param: Parameter specifying how many neighbors each candidate rectangle should have to retain it.
//5: :)
//6: Minimum possible object size. Objects smaller than that are ignored (if you set a very small minimum value, your app will run heavily).
//7: Maximum possible object size. Objects larger than that are ignored. Both minimum and maximum should be set carefully to avoid slow running of the app.
Rect[] facesArray = faces.toArray(); //array of faces
for (int i = 0; i < facesArray.length; i++) {
/* Imgproc.rectangle(mRgba, facesArray[i].tl(), facesArray[i].br(),
FACE_RECT_COLOR, 3);*/
//Now draw rectangles around the obtained faces, and a circle at each rectangle center.
//mrgba in the line bellow means that the rectangle should be drawn on the colored img.
//facesArray[i].tl() returns a Point: Template class for 2D points specified by its coordinates x and y -> Template class
// facesArray[i].x and facesArray[i].y are the x and y coords of the top left top corner.
Core.rectangle(mRgba, facesArray[i].tl(), facesArray[i].br(), FACE_RECT_COLOR, 3);
//calculate the center in x and y coords.
xCenter = (facesArray[i].x + facesArray[i].width + facesArray[i].x) / 2;
yCenter = (facesArray[i].y + facesArray[i].y + facesArray[i].height) / 2;
Point center = new Point(xCenter, yCenter); //store the center.
//Imgproc.circle(mRgba, center, 10, new Scalar(255, 0, 0, 255), 3);
Core.circle(mRgba, center, 10, new Scalar(255, 0, 0, 255), 3); //draw a red circle at the center of the face rectangle.
/*Imgproc.putText(mRgba, "[" + center.x + "," + center.y + "]",
new Point(center.x + 20, center.y + 20),
Core.FONT_HERSHEY_SIMPLEX, 0.7, new Scalar(255, 255, 255,
255));*/
//write the coordinates of the rectangle center:
Core.putText(mRgba, "[" + center.x + "," + center.y + "]",
new Point(center.x + 20, center.y + 20) , // this is the bottom left corner of the text string
Core.FONT_HERSHEY_SIMPLEX, 0.7, new Scalar(255, 255, 255,
255));
Rect r = facesArray[i]; //get the currect face, we want to use it to detect the eyes inside it.
// compute the eye area
//Rect (x, y, w, h)
Rect eyearea = new Rect(r.x + r.width / 8,
(int) (r.y + (r.height / 4.5)), r.width - 2 * r.width / 8,
(int) (r.height / 3.0));
// split it
Rect eyearea_right = new Rect(r.x + r.width / 16,
(int) (r.y + (r.height / 4.5)),
(r.width - 2 * r.width / 16) / 2, (int) (r.height / 3.0));
Rect eyearea_left = new Rect(r.x + r.width / 16
+ (r.width - 2 * r.width / 16) / 2,
(int) (r.y + (r.height / 4.5)),
(r.width - 2 * r.width / 16) / 2, (int) (r.height / 3.0));
// draw the area - mGray is working grayscale mat, if you want to
// see area in rgb preview, change mGray to mRgba
/*Imgproc.rectangle(mRgba, eyearea_left.tl(), eyearea_left.br(),
new Scalar(255, 0, 0, 255), 2);
Imgproc.rectangle(mRgba, eyearea_right.tl(), eyearea_right.br(),
new Scalar(255, 0, 0, 255), 2);*/
Core.rectangle(mRgba, eyearea_left.tl(), eyearea_left.br(),
new Scalar(255, 0, 0, 255), 2);
Core.rectangle(mRgba, eyearea_right.tl(), eyearea_right.br(),
new Scalar(255, 0, 0, 255), 2);
if (learn_frames < 5) {
// no learned frames -> Learn templates from at least 5 frames..
templateR = get_template(mJavaDetectorEye, eyearea_right, 24);
templateL = get_template(mJavaDetectorEye, eyearea_left, 24);
learn_frames++;
} else {
// Learning finished, use the new templates for template
// matching
match_eye(eyearea_right, templateR, method);
match_eye(eyearea_left, templateL, method);
}
// cut eye areas and put them to zoom windows
Imgproc.resize(mRgba.submat(eyearea_left), mZoomWindow2,
mZoomWindow2.size());
Imgproc.resize(mRgba.submat(eyearea_right), mZoomWindow,
mZoomWindow.size());
}
return mRgba;
}
private void setMinFaceSize(float faceSize) {
mRelativeFaceSize = faceSize;
mAbsoluteFaceSize = 0;
}
private void CreateAuxiliaryMats() {
if (mGray.empty())
return;
int rows = mGray.rows();
int cols = mGray.cols();
if (mZoomWindow == null) {
mZoomWindow = mRgba.submat(rows / 2 + rows / 10, rows, cols / 2
+ cols / 10, cols);
mZoomWindow2 = mRgba.submat(0, rows / 2 - rows / 10, cols / 2
+ cols / 10, cols);
}
}
private void match_eye(Rect area, Mat mTemplate, int type) {
Point matchLoc;
Mat mROI = mGray.submat(area);
int result_cols = mROI.cols() - mTemplate.cols() + 1;
int result_rows = mROI.rows() - mTemplate.rows() + 1;
// Check for bad template size
if (mTemplate.cols() == 0 || mTemplate.rows() == 0) {
return ;
}
Mat mResult = new Mat(result_cols, result_rows, CvType.CV_8U);
switch (type) {
case TM_SQDIFF:
Imgproc.matchTemplate(mROI, mTemplate, mResult, Imgproc.TM_SQDIFF);
break;
case TM_SQDIFF_NORMED:
Imgproc.matchTemplate(mROI, mTemplate, mResult,
Imgproc.TM_SQDIFF_NORMED);
break;
case TM_CCOEFF:
Imgproc.matchTemplate(mROI, mTemplate, mResult, Imgproc.TM_CCOEFF);
break;
case TM_CCOEFF_NORMED:
Imgproc.matchTemplate(mROI, mTemplate, mResult,
Imgproc.TM_CCOEFF_NORMED);
break;
case TM_CCORR:
Imgproc.matchTemplate(mROI, mTemplate, mResult, Imgproc.TM_CCORR);
break;
case TM_CCORR_NORMED:
Imgproc.matchTemplate(mROI, mTemplate, mResult,
Imgproc.TM_CCORR_NORMED);
break;
}
Core.MinMaxLocResult mmres = Core.minMaxLoc(mResult);
// there is difference in matching methods - best match is max/min value
if (type == TM_SQDIFF || type == TM_SQDIFF_NORMED) {
matchLoc = mmres.minLoc;
} else {
matchLoc = mmres.maxLoc;
}
Point matchLoc_tx = new Point(matchLoc.x + area.x, matchLoc.y + area.y);
Point matchLoc_ty = new Point(matchLoc.x + mTemplate.cols() + area.x,
matchLoc.y + mTemplate.rows() + area.y);
/*Imgproc.rectangle(mRgba, matchLoc_tx, matchLoc_ty, new Scalar(255, 255, 0,
255));*/
Core.rectangle(mRgba, matchLoc_tx, matchLoc_ty, new Scalar(255, 255, 0,
255));
Rect rec = new Rect(matchLoc_tx,matchLoc_ty);
}
private Mat get_template(CascadeClassifier clasificator, Rect area, int size) {
Mat template = new Mat(); //prepare a Mat which will serve as a template for eyes.
Mat mROI = mGray.submat(area); //detect only region of interest which is represented by the area. So, from the total Mat get only the submat that represent roi.
MatOfRect eyes = new MatOfRect(); //will be around eyes (including eyes), this will be filled by detectMultiScale
Point iris = new Point(); //to identify iris.
Rect eye_template = new Rect();
clasificator.detectMultiScale(mROI, eyes, 1.15, 2,
Objdetect.CASCADE_FIND_BIGGEST_OBJECT
| Objdetect.CASCADE_SCALE_IMAGE, new Size(30, 30),
new Size());
Rect[] eyesArray = eyes.toArray(); //get the detected eyes
for (int i = 0; i < eyesArray.length;) {
Rect e = eyesArray[i];
e.x = area.x + e.x; //the starting x coordinates of the rect (area) around the eye + the area
e.y = area.y + e.y;
Rect eye_only_rectangle = new Rect((int) e.tl().x,
(int) (e.tl().y + e.height * 0.4), (int) e.width,
(int) (e.height * 0.6));
mROI = mGray.submat(eye_only_rectangle);
Mat vyrez = mRgba.submat(eye_only_rectangle);
Core.MinMaxLocResult mmG = Core.minMaxLoc(mROI);
// Imgproc.circle(vyrez, mmG.minLoc, 2, new Scalar(255, 255, 255, 255), 2);
Core.circle(vyrez, mmG.minLoc, 2, new Scalar(255, 255, 255, 255), 2);
iris.x = mmG.minLoc.x + eye_only_rectangle.x;
iris.y = mmG.minLoc.y + eye_only_rectangle.y;
eye_template = new Rect((int) iris.x - size / 2, (int) iris.y
- size / 2, size, size);
/*Imgproc.rectangle(mRgba, eye_template.tl(), eye_template.br(),
new Scalar(255, 0, 0, 255), 2);*/
Core.rectangle(mRgba, eye_template.tl(), eye_template.br(),
new Scalar(255, 0, 0, 255), 2);
template = (mGray.submat(eye_template)).clone();
return template;
}
return template;
}
public void onRecreateClick(View v)
{
learn_frames = 0;
}
}
Notice that the camera opens successfully for recording, and the xml files are loaded, but nothing happens after that. I made the window size as 100 x 100 just for testing purposes, I know it should be 1 x 1.
Can anyone please tell me how to solve this problem? How can I run opencv video camera for face and eye tracking from background service?
I tried to get the opencv camera in a service as you are doing but I was unable to get neither onCameraFrame nor onCameraViewStarted callbacks, which meant that the camera was not getting initialized. After a bunch of tries:
Setting the preview to INVISIBLE/GONE -> not working
Setting the preview size to a pixel size of 1×1 or respecting
camera's aspect ratio 4x3 ->not working
Setting the preview outside the screen -> not working
I found out that opencv camera needs to be previewed with view's size, only that way I was able to get onCameraFrame callback.
Fortunately, I could place another element on top of the camera preview to hide it, and show the alarms only.
You could find a simple CameraInService example here, hope it is useful for you.

How to perform object visibility test on Android OpenGL ES

I have a world full of 2D squares (z=0). At startup I setup projection in such a way that the whole world is visible on the screen, using:
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(left, right, bottom, top, zNear, zFar);
Then I allow the user to zoom in the world with fingers by using:
gl.glScalef(mScaleFactor, mScaleFactor, 1.0f);
I want to make visibility test for objects that appear not visible as the user is zooming in to not render them (performance boost).
I found this method:
android.opengl.Visibility.visibilityTest(float[] ws, int wsOffset, float[] positions, int positionsOffset, char[] indices, int indicesOffset, int indexCount);
But I can't make it work, nor I found ANY examples of the usage of this method on Internet. Currently this method returns result=0 for every square I test, even when scaling is not applied (mScaleFactor = 1.0)
The way I'm doing this:
final short SQUARE_VERTICES_ORDER_TEMPLATE[] = {0, 1, 2, 0, 2, 3};
.....
float[] vertices = toArray(mVertexBuffer);
short[] indices = toArray(mIndicesBuffer);
char[] charIndices = new char[indices.length];
// method needs char[]
for (int i = 0; i < indices.length; i++) {
short shortIndex = indices[i];
charIndices[i] = (char) shortIndex;
}
for (int i = 0; i < mSquares.size(); i++) {
int numIndicesPerSquare = SQUARE_VERTICES_ORDER_TEMPLATE.length;
int indicesOffset = i * numIndicesPerSquare;
int result = Visibility.visibilityTest(matrixGrabber.mProjection, 0, vertices, 0, charIndices, indicesOffset, numIndicesPerSquare);
switch (result) {
case 0:
Log.v(TAG, "Object NOT VISIBLE: " + mSquares.get(i)); // hits every time
break;
case 1:
Log.v(TAG, "Object PARTIALLY VISIBLE: " + mSquares.get(i));
break;
default:
TAG.toString(); // to place a break point
break;
}
}
I'm not sure if I'm picking up the right Matrix required by this method.
Could you please validate the right usage of this method or give any other tips or workarounds ?
I've figured out what is wrong, visibilityTest method requires multiplied matrix. Here is how it should be:
matrixGrabber.getCurrentState(gl);
float[] resultMatrix = new float[matrixGrabber.mProjection.length];
Matrix.multiplyMM(resultMatrix, 0, matrixGrabber.mProjection, 0, matrixGrabber.mModelView, 0);
....
Visibility.visibilityTest(resultMatrix, 0, vertices, 0, charIndices, indicesOffset, numIndicesPerSquare);

Adding Endless parallax background in cocos2d android

I am working on CoCos2d with android.I want to add an endless scrolling background to my Screen by using CCParallaxNode.
I am able to add background and move it but after the completion of that move action the screen goes black.
Can someone help me out?
My code is
CCParallaxNode parallaxNode;
CCSprite spacedust1;
CCSprite spacedust2;
CCSprite planetsunrise;
CCSprite galaxy;
CCSprite spacialanomaly;
CCSprite spacialanomaly2;
parallaxNode = CCParallaxNode.node();
spacedust1 = CCSprite.sprite("bg_front_spacedust.png");
spacedust2 = CCSprite.sprite("bg_front_spacedust.png");
planetsunrise = CCSprite.sprite("bg_planetsunrise.png");
galaxy = CCSprite.sprite("bg_galaxy.png");
spacialanomaly = CCSprite.sprite("bg_spacialanomaly.png");
spacialanomaly2 = CCSprite.sprite("bg_spacialanomaly2.png");
// 3) Determine relative movement speeds for space dust and background
// CGPoint cgPoint = CGPoint.ccp(0.1, 0.1);
CGPoint dustSpeed = CGPoint.ccp(10, 10);
CGPoint bgSpeed = CGPoint.ccp(5, 5);
// CGPoint bgSpeed = ccp(0.05, 0.05);
parallaxNode.addChild(spacedust1, 0, dustSpeed.x, dustSpeed.y, 0,
winSize.height / 2);
parallaxNode.addChild(spacedust2, 0, dustSpeed.x, dustSpeed.y,
spacedust1.getContentSize().width, winSize.height / 2);
parallaxNode.addChild(galaxy, -1, bgSpeed.x, bgSpeed.y, 0, 10);
parallaxNode.addChild(planetsunrise, -1, bgSpeed.x, bgSpeed.y, 600, 5);
parallaxNode
.addChild(spacialanomaly, -1, bgSpeed.x, bgSpeed.y, 900, 20);
parallaxNode.addChild(spacialanomaly2, -1, bgSpeed.x, bgSpeed.y, 1500,
30);
CCIntervalAction go = CCMoveBy.action(4, CGPoint.ccp(winSize.width, 0));
CCIntervalAction goBack = go.reverse();
CCIntervalAction seq = CCSequence.actions(go, goBack);
CCRepeatForever action = CCRepeatForever.action(goBack);
parallaxNode.runAction(action);
I see that since not a single answer worked for you. I will provide a simple code which will help you for your parralax scrolling background.
Add this code in your game layers constructor
background1 = CCSprite.sprite("bg2.png");
background2 = CCSprite.sprite("bg2.png");
background1.setPosition(CGPoint.ccp(winSize.width*0.5f,winSize.height*0.5f));
addChild(background1);
background2.setPosition(CGPoint.ccp(winSize.width+winSize.width*0.5f,winSize.height*0.5f));
addChild(background2);
and a scroll method which is scheduled every millisecond.
add this in constructor
this.schedule("scroll");
and now the scroll method.
public void scroll(float dt) {
CGPoint pos1 = background1.getPosition();
CGPoint pos2 = background2.getPosition();
pos1.x -= 5.0f;
pos2.x -= 5.0f;
if(pos1.x <=-(winSize.width*0.5f) )
{
pos1.x = pos2.x + winSize.width;
}
if(pos2.x <=-(winSize.width*0.5f) )
{
pos2.x = pos1.x + winSize.width;
}
background1.setPosition(pos1);
background2.setPosition(pos2);
}
mark my answer if it worked.
Call this method from the class Constructor. I found this trick from Example : "shotingblock-master" available on github...
private void endlessBackground() {
// Create the two background sprites which will alternate
_oddBackground = CCSprite.sprite("blue_background.png");
_evenBackground = CCSprite.sprite("blue_background.png");
// One starts dead centre and one starts exactly one screen height above
oddBackground.setPosition(_winSize.width / 2, _winSize.height / 2);
evenBackground.setPosition(_winSize.width / 2, _winSize.height
+ (_winSize.height / 2));
// Schedule the scrolling action
schedule("scroll");
// Add sprites to the layer
addChild(_oddBackground).addChild(_evenBackground);
}
public void scroll(float dt) {
// move them 100*dt pixels down
_oddBackground.setPosition(_oddBackground.getPosition().x,
_oddBackground.getPosition().y - 150 * dt);
_evenBackground.setPosition(_evenBackground.getPosition().x,
_evenBackground.getPosition().y - 150 * dt);
// reset position when they are off from view.
if (_oddBackground.getPosition().y < -_winSize.height / 2) {
_oddBackground.setPosition(_winSize.width / 2, _winSize.height / 2);
_evenBackground.setPosition(_winSize.width / 2, _winSize.height
+ (_winSize.height / 2));
}
}
}
IT works excellent in my case. May be it'll help full for you.
try using this:
CCTexture2D *texture = CCTextureCache::sharedTextureCache()->addImage("pic.png");
ccTexParams params = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT};
texture->setTexParameters(&params);
CCSprite *sprite = CCSprite::spriteWithTexture(texture, CCRectMake(0, 0, 90, 90));
and make sure that The image's height and width must be power of 2.
It looks like the CCRepeatForever action is only running it goBack, which means that it's not reversing. Try the following:
CCIntervalAction go = CCMoveBy.action(4, CGPoint.ccp(winSize.width, 0));
CCIntervalAction goBack = go.reverse();
CCIntervalAction seq = CCSequence.actions(go, goBack);
CCRepeatForever action = CCRepeatForever.action(seq); // change to seq instead of goBack
parallaxNode.runAction(action);
This is trick for make it happen. You can Use the large png and working on it or check the sample test code which is available in coocs2d-android library
CCSprite background = CCSprite.sprite("background.png");
// create a void node, a parent node
CCParallaxNode voidNode = CCParallaxNode.node();
// background image is moved at a ratio of 0.4x, 0.5y
voidNode.addChild(background, -1, 0.4f, 0.5f, 0, 0);
// write your own code for the parallax node
CCIntervalAction goUp = CCMoveBy.action(4, CGPoint.make(0,-200));
CCIntervalAction goDown = goUp.reverse();
CCIntervalAction go = CCMoveBy.action(8, CGPoint.make(-1000, 0));
CCIntervalAction goBack = go.reverse();
CCIntervalAction seq = CCSequence.actions(goUp, go, goDown, goBack);
voidNode.runAction(CCRepeatForever.action(seq));
addChild(voidNode);
Please check out below link for Parallax vertical endless background:
http://kalpeshsantoki.blogspot.in/2014/07/create-vertical-endless-parallax.html
CGSize winSize = CCDirector.sharedDirector().displaySize();
//I made graphics for screen 720*1200....so I made this dynamic scale to support multiple screens
float sX = winSize.width / 720.0f;
float sY = winSize.height / 1200.0f;
background = CCVerticalParallaxNode.node(sX, sY, true);
background.addEntity(1f, "background.png", 0);
background.addEntity(3, "road_simple.png", winSize.width / 2);
background.addEntity(1.7f, "road_side.png", 0);
addChild(background);

Android Toast in iPhone?

When I write Android apps, I love the Toast feature. Is there a way to get this kind of set and forget popup message in iPhone development using MonoTouch (C# .NET)?
MonoTouch Toast Version here. Inspired by Android.
To call it,
ToastView t = new ToastView ("Email Sent", 1000);
t.Show ();
Enum File:
public enum ToastGravity
{
Top = 0,
Bottom = 1,
Center = 2
}
ToastSettings File:
using System;
using System.Drawing;
using MonoTouch.UIKit;
namespace General
{
public class ToastSettings
{
public ToastSettings ()
{
this.Duration = 500;
this.Gravity = ToastGravity.Center;
}
public int Duration
{
get;
set;
}
public double DurationSeconds
{
get { return (double) Duration/1000 ;}
}
public ToastGravity Gravity
{
get;
set;
}
public PointF Position
{
get;
set;
}
}
}
Main Toast Class:
using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System.Drawing;
using MonoTouch.ObjCRuntime;
namespace General
{
public class ToastView : NSObject
{
ToastSettings theSettings = new ToastSettings ();
private string text = null;
UIView view;
public ToastView (string Text, int durationMilliseonds)
{
text = Text;
theSettings.Duration = durationMilliseonds;
}
int offsetLeft = 0;
int offsetTop = 0;
public ToastView SetGravity (ToastGravity gravity, int OffSetLeft, int OffSetTop)
{
theSettings.Gravity = gravity;
offsetLeft = OffSetLeft;
offsetTop = OffSetTop;
return this;
}
public ToastView SetPosition (PointF Position)
{
theSettings.Position = Position;
return this;
}
public void Show ()
{
UIButton v = UIButton.FromType (UIButtonType.Custom);
view = v;
UIFont font = UIFont.SystemFontOfSize (16);
SizeF textSize = view.StringSize (text, font, new SizeF (280, 60));
UILabel label = new UILabel (new RectangleF (0, 0, textSize.Width + 5, textSize.Height + 5));
label.BackgroundColor = UIColor.Clear;
label.TextColor = UIColor.White;
label.Font = font;
label.Text = text;
label.Lines = 0;
label.ShadowColor = UIColor.DarkGray;
label.ShadowOffset = new SizeF (1, 1);
v.Frame = new RectangleF (0, 0, textSize.Width + 10, textSize.Height + 10);
label.Center = new PointF (v.Frame.Size.Width / 2, v.Frame.Height / 2);
v.AddSubview (label);
v.BackgroundColor = UIColor.FromRGBA (0, 0, 0, 0.7f);
v.Layer.CornerRadius = 5;
UIWindow window = UIApplication.SharedApplication.Windows[0];
PointF point = new PointF (window.Frame.Size.Width / 2, window.Frame.Size.Height / 2);
if (theSettings.Gravity == ToastGravity.Top)
{
point = new PointF (window.Frame.Size.Width / 2, 45);
}
else if (theSettings.Gravity == ToastGravity.Bottom)
{
point = new PointF (window.Frame.Size.Width / 2, window.Frame.Size.Height - 45);
}
else if (theSettings.Gravity == ToastGravity.Center)
{
point = new PointF (window.Frame.Size.Width / 2, window.Frame.Size.Height / 2);
}
else
{
point = theSettings.Position;
}
point = new PointF (point.X + offsetLeft, point.Y + offsetTop);
v.Center = point;
window.AddSubview (v);
v.AllTouchEvents += delegate { HideToast (null); };
NSTimer.CreateScheduledTimer (theSettings.DurationSeconds, HideToast);
}
void HideToast ()
{
UIView.BeginAnimations ("");
view.Alpha = 0;
UIView.CommitAnimations ();
}
void RemoveToast ()
{
view.RemoveFromSuperview ();
}
}
}
Check this out:
https://github.com/ecstasy2/toast-notifications-ios
Edit: The project has moved to github so i update the link.
Here's my version: http://github.com/scalessec/toast
I think it's simpler to use because it's implemented as a obj-c category, thereby adding the makeToast methods to any instance of UIView. eg:
[self.view makeToast:#"This is some message as toast."
duration:3.0
position:#"bottom"];
Are you looking for something like UIAlertView?
You can use this link for objective-c code for Toast
http://code.google.com/p/toast-notifications-ios/source/browse/trunk/
While this link for its usage
http://code.google.com/p/toast-notifications-ios/wiki/HowToUse
which could be like any one of the below samples
[[iToast makeText:NSLocalizedString(#"The activity has been successfully saved.", #"")] show];
[[[iToast makeText:NSLocalizedString(#"The activity has been successfully saved.", #"")]
setGravity:iToastGravityBottom] show];
[[[[iToast makeText:NSLocalizedString(#"Something to display a very long time", #"")]
etGravity:iToastGravityBottom] setDuration:iToastDurationLong] show];
You might be after Local Notifications, pretty sure they allow you to set a time, I think in epoch time to be fired off. Don't think there is a way to hide them though. I might be misunderstanding your question though cause I'm unfamiliar with Toast.
Just You can use the following code with uilabel and uianimation to get toast like in android.
It does two works one is toast task and it increases the height of the label according to the text length with wordwrap IOS 7 later link here
CGRect initialFrame = CGRectMake(20, self.view.frame.size.height/2,300, 40);
NSString *message=#"Toast in Iphone as in Android";
UILabel *flashLabel=[[UILabel alloc] initWithFrame:initialFrame];
flashLabel.font=[UIFont fontWithName:#"Optima-Italic" size:12.0];
flashLabel.backgroundColor=[UIColor whiteColor];
flashLabel.layer.cornerRadius=3.0f;
flashLabel.numberOfLines=0;
flashLabel.textAlignment=NSTextAlignmentCenter;
CGSize maxSize = CGSizeMake(flashLabel.frame.size.width, MAXFLOAT);
CGRect labelRect = [message boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName:flashLabel.font} context:nil];
//adjust the label the the new height.
CGRect newFrame = flashLabel.frame;
newFrame.size.height = labelRect.size.height;
flashLabel.frame = newFrame;
flashLabel.text=message;
[self.view addSubview:flashLabel];
flashLabel.alpha=1.0;
self.view.userInteractionEnabled=FALSE;
[UIView animateWithDuration:13.0 animations:^
{
flashLabel.alpha=0.0f;
}
completion:^(BOOL finished)
{
self.view.userInteractionEnabled=TRUE;
[flashLabel removeFromSuperview];
}];
I modified John's answer as follows:
Toast.h
#interface Toast : NSObject
+ (void)toast:(NSString *)message
:(UIView *) view
:(int)delay;
#end
Toast.m
#import "Toast.h"
#interface Toast ()
#end
#implementation Toast
+ (void)toast:(NSString *)message
:(UIView *) view
:(int)delay
{
CGRect initialFrame = CGRectMake(10, view.frame.size.height/2, 300, 40);
UILabel *flashLabel=[[UILabel alloc] initWithFrame:initialFrame];
flashLabel.font=[UIFont fontWithName:#"Optima-Italic" size:19.0];
flashLabel.backgroundColor=[UIColor whiteColor];
flashLabel.layer.cornerRadius=9.0f;
flashLabel.clipsToBounds = YES;
flashLabel.numberOfLines=3;
flashLabel.textAlignment=NSTextAlignmentCenter;
CGSize maxSize = CGSizeMake(flashLabel.frame.size.width, MAXFLOAT);
CGRect labelRect = [message boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:flashLabel.font}
context:nil];
//adjust the label the the new height.
CGRect newFrame = flashLabel.frame;
newFrame.size.height = labelRect.size.height * 2;
flashLabel.frame = newFrame;
flashLabel.text=message;
[view addSubview:flashLabel];
flashLabel.alpha=1.0;
view.userInteractionEnabled=FALSE;
[UIView animateWithDuration:delay animations:^
{
flashLabel.alpha=0.0f;
}
completion:^(BOOL finished)
{
view.userInteractionEnabled=TRUE;
[flashLabel removeFromSuperview];
}];
}
#end
I have added a little modification to the toast class that handles rotation of the display.
public void Show ()
{
UIButton v = UIButton.FromType (UIButtonType.Custom);
view = v;
UIFont font = UIFont.SystemFontOfSize (16);
SizeF textSize = view.StringSize (text, font, new SizeF (280, 60));
UILabel label = new UILabel (new RectangleF (0, 0, textSize.Width + 5, textSize.Height + 5));
label.BackgroundColor = UIColor.Clear;
label.TextColor = UIColor.White;
label.Font = font;
label.Text = text;
label.Lines = 0;
label.ShadowColor = UIColor.DarkGray;
label.ShadowOffset = new SizeF (1, 1);
v.Frame = new RectangleF (0, 0, textSize.Width + 10, textSize.Height + 10);
label.Center = new PointF (v.Frame.Size.Width / 2, v.Frame.Height / 2);
v.AddSubview (label);
v.BackgroundColor = UIColor.FromRGBA (0, 0, 0, 0.7f);
v.Layer.CornerRadius = 5;
UIWindow window = UIApplication.SharedApplication.Windows[0];
PointF point = new PointF (window.Frame.Size.Width / 2, window.Frame.Size.Height / 2);
if (theSettings.Gravity == ToastGravity.Top)
{
point = new PointF (window.Frame.Size.Width / 2, 45);
}
else if (theSettings.Gravity == ToastGravity.Bottom)
{
point = new PointF (window.Frame.Size.Width / 2, window.Frame.Size.Height - 45);
}
else if (theSettings.Gravity == ToastGravity.Center)
{
point = new PointF (window.Frame.Size.Width / 2, window.Frame.Size.Height / 2);
}
else
{
point = theSettings.Position;
}
point = new PointF (point.X + offsetLeft, point.Y + offsetTop);
v.Center = point;
//handle screen rotation
float orientation=0;
switch(UIApplication.SharedApplication.StatusBarOrientation)
{
case UIInterfaceOrientation.LandscapeLeft:
orientation=-90;
break;
case UIInterfaceOrientation.LandscapeRight:
orientation=90;
break;
case UIInterfaceOrientation.PortraitUpsideDown:
orientation=180;
break;
}
v.Transform=CGAffineTransform.MakeRotation ((float)(orientation / 180f * Math.Pi));
window.AddSubview (v);
v.AllTouchEvents += delegate { HideToast (); };
NSTimer.CreateScheduledTimer (theSettings.DurationSeconds, HideToast);
}
You could try my open source library TSMessages: https://github.com/toursprung/TSMessages
It's really easy to use and looks beautiful on iOS 5/6 and on iOS 7 as well.
I really like MonoTouch solution proposed by Bahai.
The following is not a substitution. Is just a ready-to-go one method the worked for me.
private async Task ShowToast(string message, UIAlertView toast = null)
{
if (null == toast)
{
toast = new UIAlertView(null, message, null, null, null);
toast.Show();
await Task.Delay(2000);
await ShowToast(message, toast);
return;
}
UIView.BeginAnimations("");
toast.Alpha = 0;
UIView.CommitAnimations();
toast.DismissWithClickedButtonIndex(0, true);
}
If the method is called from a background thread (not the main UI thread) then BeginInvokeOnMainThread is required which means just call it like this.
BeginInvokeOnMainThread(() =>
{
ShowToast(message);
});
I created a new repo on github with a class to do iOS toast-style alerts. I didn't like the one on code.google.com, it didn't rotate properly and wasn't pretty.
https://github.com/esilverberg/ios-toast
Enjoy folks.

Categories

Resources