I'm creating android application for shapes on canvas onClick of button.
What I want:
I have three buttons 'Square','Circle','Triangle'. Each time I tap on button and object of that shape will be created & displayed in a random position on one canvas.(done)
On tap of each shape will cause it to become another shape. tapping on a square will make it a circle tapping on a circle will make it a triangle tapping on a triangle will make it a square (done)
On long tap on a shape can delete the shape.(done)
How to implement undo functionality!
Tried and Error: To achieve this functionality I tried so far following code in which I can create one shape on click of button using bitmap in java and image view in xml.
Update_29/10: I work on same code and create multiple shapes on canvas using dynamic relative layout adding views.
My Question:
Is this right way(bitmap and imageview) to create shape on canvas by clicking on button?
I can create multiple shapes on canvas now but every time I'm creating new canvas instance!any other way to achieve this?
Update_29_10:
How to get click of shape so I can delete shape and undo functionality as well.
XML:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context=".MainActivity">
<RelativeLayout
android:id="@+id/relative4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_above="@+id/btnCircle"
android:background="@color/colorAccent">
</RelativeLayout>
<Button
android:id="@+id/btnSquare"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="@string/square" />
<Button
android:id="@+id/btnCircle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@+id/btnSquare"
android:text="@string/circle" />
<Button
android:id="@+id/btnTriangle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@+id/btnCircle"
android:text="@string/triangle" />
<Button
android:id="@+id/btnUndo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/btnTriangle"
android:layout_alignParentBottom="true"
android:text="@string/undo" />
MainActivity.java:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Context mContext;
private Resources mResources;
private RelativeLayout mRelativeLayout;
private Button btnSquare, btnCircle, btnTriangle,btnUndo,btnState;
private int mSuareCount=0,mCircleCount=0,mTriangelCount=0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews() {
mContext = getApplicationContext();
mResources = getResources();
mRelativeLayout = (RelativeLayout) findViewById(R.id.rl);
btnSquare = (Button) findViewById(R.id.btnSquare);
btnCircle = (Button)findViewById(R.id.btnCircle);
btnTriangle = (Button)findViewById(R.id.btnTriangle);
btnUndo=(Button)findViewById(R.id.btnUndo);
setOnClickListeners();
}
private void setOnClickListeners() {
btnSquare.setOnClickListener(this);
btnCircle.setOnClickListener(this);
btnTriangle.setOnClickListener(this);
btnUndo.setOnClickListener(this);
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btnSquare:
drawSquare(null);
mSuareCount++;
break;
case R.id.btnCircle:
drawCircle(null);
mCircleCount++;
break;
case R.id.btnTriangle:
drawTriangle(null);
mTriangelCount++;
break;
case R.id.btnUndo:
break;
}
}
private void drawSquare(ImageView imageView) {
Bitmap bitmap = Bitmap.createBitmap(
50, // Width
50, // Height
Bitmap.Config.ARGB_8888 // Config
);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.LTGRAY);
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.YELLOW);
paint.setAntiAlias(true);
int padding = 50;
Rect rectangle = new Rect(
padding, // Left
padding, // Top
canvas.getWidth() - padding, // Right
canvas.getHeight() - padding // Bottom
);
canvas.drawRect(rectangle, paint);
addViews(bitmap,imageView,1);
// Display the newly created bitmap on app interface
if (imageView == null) {
imageView = new ImageView(this);
}
imageView.setImageBitmap(bitmap);
final ImageView finalImageView = imageView;
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
drawCircle(finalImageView);
mSuareCount--;
mCircleCount++;
}
});
}
private void drawCircle(ImageView imageView) {
Bitmap bitmap = Bitmap.createBitmap(
50, // Width
50, // Height
Bitmap.Config.ARGB_8888 // Config
);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
paint.setAntiAlias(true);
int radius = Math.min(canvas.getWidth(), canvas.getHeight() / 2);
int padding = 5;
canvas.drawCircle(
canvas.getWidth() / 2, // cx
canvas.getHeight() / 2, // cy
radius - padding, // Radius
paint // Paint
);
addViews(bitmap,imageView,2);
// Display the newly created bitmap on app interface
if (imageView == null) {
imageView = new ImageView(this);
}
imageView.setImageBitmap(bitmap);
final ImageView finalImageView = imageView;
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
drawTriangle(finalImageView);
mCircleCount--;
mTriangelCount++;
}
});
}
private void drawTriangle(ImageView imageView) {
Bitmap bitmap = Bitmap.createBitmap(
500, // Width
500, // Height
Bitmap.Config.ARGB_8888 // Config
);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
final Rect rect = new Rect(0, 0, bitmap.getWidth(),
bitmap.getHeight());
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.YELLOW);
paint.setAntiAlias(true);
Point point1_draw = new Point(90, 0);
Point point2_draw = new Point(0, 180);
Point point3_draw = new Point(180, 180);
Path path = new Path();
path.moveTo(point1_draw.x, point1_draw.y);
path.lineTo(point2_draw.x, point2_draw.y);
path.lineTo(point3_draw.x, point3_draw.y);
path.lineTo(point1_draw.x, point1_draw.y);
path.close();
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(Color.parseColor("#3F51B5"));
canvas.drawPath(path, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
//addViews(bitmap,imageView);
addViews(bitmap,imageView,3);
if (imageView == null) {
imageView = new ImageView(this);
}
imageView.setImageBitmap(bitmap);
final ImageView finalImageView = imageView;
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
drawSquare(finalImageView);
mSuareCount++;
mTriangelCount--;
}
});
}
private void addViews(Bitmap bitmap, ImageView imageView, final int value) {
final int min = 20;
final int max = 80;
Drawable d = getResources().getDrawable(R.mipmap.ic_launcher_round);
final int w = d.getIntrinsicWidth();
final int random = new Random().nextInt((max - min) + 1) + min;
RelativeLayout relative4 = (RelativeLayout) findViewById(R.id.relative4);
int width = relative4.getMeasuredWidth();
int height = relative4.getMeasuredHeight();
if (imageView == null) {
imageView = new ImageView(this);
}
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100);
params.setMargins(new Random().nextInt((width - 0) + 1), new Random().nextInt((height - 0) + 1), 10, 10);
imageView.setLayoutParams(params);
imageView.setImageBitmap(bitmap);
if (imageView != null) {
ViewGroup parent = (ViewGroup) imageView.getParent();
if (parent != null) {
parent.removeView(imageView);
}
}
relative4.addView(imageView);
final ImageView finalImageView = imageView;
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (value) {
case 1:
drawCircle(finalImageView);
mSuareCount--;
mCircleCount++;
break;
case 2:
drawTriangle(finalImageView);
mCircleCount--;
mTriangelCount++;
break;
case 3:
drawSquare(finalImageView);
mTriangelCount--;
mSuareCount++;
break;
}
}
});
imageView.setOnLongClickListener(new View.OnLongClickListener(){
@Override
public boolean onLongClick(View v) {
switch (value) {
case 1:
relative4.removeView(finalImageView);
mSquareCount--;
break;
case 2:
relative4.removeView(finalImageView);
mCircleCount--;
break;
case 3:
relative4.removeView(finalImageView);
mTriangleCount--;
break;
}
return true;
});
}
}
While aanshu's solution is probably the more efficient way to do this, his solution makes it difficult to recognize clicks on the single shapes.
Instead of making one big subclass of ImageView and overriding onDraw and drawing each of the sub-shapes, you could make a subclass of ImageView for each class of square, circle and triangle. Something like this:
public class SquareView extends ImageView {
@Override
protected void onDraw(Canvas canvas) {
//draw your Square on the canvas given
}
}
Same for Circle and triangle. Then just generate one of these views as you were generating image views before but do not set the Bitmap. You can add them to the layout and it will call the onDraw function and give it a canvas. Now you can register your onClickListener for each of these views as before. When the view is clicked you replace it with an instance of a different class, for example you replace SquareView with a CircleView.
For the undo operations, you could just make this. This is very pseudocode like, I also might be mixing some programming languages but the idea is everywhere the same. If something is unclear, please ask.
Stack<Runnable> undoStack;
//and now whenever you do something that should be undoable you just add a runnable that will undo it:
//for example if a user clicked a SquareView:
removeView(squareView);
CircleView circleView = new CircleView();
//take the information from the squareView that is needed
circleview.position = squareView.position;
addView(circleView);
undoStack.push(() -> removeView(circleView); addView(squareView););
//When you undo you just call
undoStack.pop().run();
A bit of background about your code, because I think it will help you understand. This is the source of android ImageView. When you call setImageBitmap(bitmap), it passes the bitmap to an instance of BitmapDrawable which it calls mDrawable. Then in the ImageViews onDraw(Canvas canvas) method it calls mDrawable.draw(canvas) which in case of BitmapDrawable (source here) after a lot of other stuff calls canvas.drawBitmap(bitmap). So basically what your code does: it creates a Bitmap from a canvas and then via ImageView and BitmapDrawable the bitmap is drawn back on a canvas. My and aanshu's solution draw directly on this final canvas. That is why they are better than your current solution.
Edit: While searching something else, I stumbled upon drawable shape resources. As with any drawable resource you can just pass them to ImageViews. This way you probably wouldn't have to override the onDraw function. But I never worked with them, so I'll just leave this here.