Analyse and handle scroll gestures in Imageview (with canvas) without a ScrollView - android

I've tried many things to handle my problem, but it didn't really work and google also couldn't help me.
I've an ImageView which contains a canvas. The canvas draws a part of a graph. So when the user scrolls (horizontally) the canvas should draw another part of the graph. For example:
The canvas width is 200px
The canvas draws the graph from x=0 to x=200
The user scrolls (horizontally), he moves his finger over 100px
The canvas should now draw the graph from x=100 to x=300
The canvas should redraw by every pixel the user scrolls (for a smooth scrolling)
This solution did not work:
Draw the whole graph and put it in a Scrollview. This delivers a OutOfMemoryError, because the canvas/bitmap is too big.
Also I need to do some other things, so I need to do this like I described above.
Here is the Code:
XML:
<de.touristenfahrerforum.Marcel.Fragments.SpeedGraph
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="#+id/graph"
android:background="#android:color/black"
android:name="de.touristenfahrerforum.MarcelMoiser.Fragments.SpeedGraph">
</de.touristenfahrerforum.Marcel.Fragments.SpeedGraph>
Java Class:
public class SpeedGraph extends ImageView
{
...
public SpeedGraph(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SpeedGraph(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SpeedGraph(Context context) {
super(context);
}
public void setup( ArrayList<Location> locations, ScrollViewListener scrollViewListener )
{
...
Bitmap bitmap = Bitmap.createBitmap(speedCanvasWidth,speedCanvasHeight,Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
this.setImageBitmap(bitmap);
...}
...
private void drawGraph(int start, int end, int color)
{
graphPaint.setColor(color);
long startTime = locations.get(start).getTime();
Path path = new Path();
path.moveTo((locations.get(start).getTime()-startTime)/10*V.LOGICAL_DENSITY, speedCanvasHeight-(int)(locations.get(start).getSpeed()*3.6*SIZE_FACTOR));
for( int it = start; it < end; it++ )
{
Location location = locations.get(it);
path.lineTo((location.getTime()-startTime)/10*V.LOGICAL_DENSITY, speedCanvasHeight-(int)(location.getSpeed()*3.6*SIZE_FACTOR));
}
canvas.drawPath(path,graphPaint);
}
...
So I would like to implement something that recognizes horizontal scroll gestures and gives me a number of pixels that shows me the pixels the user has scrolled.
Thank you guys in advance

It's the first time that nobody answered, but i figured out how to solve my problem.
The best way is to implement GestureDetector.OnGestureListener and overwrite all it's methods. Also you have to create a GestureDetector Object. The method OnScroll(...) is where to look for the pixels the user has scrolled.
Here is some code:
public class SpeedGraph extends ImageView implements GestureDetector.OnGestureListener
{
private Context context;
...
public void setup( ... )
{
this.gestureDetector = new GestureDetector(context, this);
gestureDetector.setIsLongpressEnabled(false);
...}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
{
scrolled+=distanceX;
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
drawGrid();
redrawGraphsInCanvas();
this.invalidate();
return true;
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev){
gestureDetector.onTouchEvent(ev);
return true;
}
...

Related

onDraw and invalidate method

I'm doing a school project. In this project I have to do a program that have one or more ball bouncing in the screen. I did some research on google to help me in this, and I found this code :
public class BouncingBallInside extends View {
private List<Ball> balls = new ArrayList<>();
public BouncingBallInside(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BouncingBallInside(Context context) {
super(context);
init();
}
private void init(){
//Add a new ball to the view
balls.add(new Ball(50,50,100, Color.RED));
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//Draw the balls
for(Ball ball : balls){
//Move first
ball.move(canvas);
//Draw them
canvas.drawOval(ball.oval,ball.paint);
}
invalidate(); // See note
}
}
The ball class :
public class Ball{
public int x,y,size;
public int velX = 10;
public int velY=7;
public Paint paint;
public RectF oval;
public Ball(int x, int y, int size, int color){
this.x = x;
this.y = y;
this.size = size;
this.paint = new Paint();
this.paint.setColor(color);
}
public void move(Canvas canvas) {
this.x += velX;
this.y += velY;
this.oval = new RectF(x-size/2,y-size/2,x+size/2,y+size/2);
//Do we need to bounce next time?
Rect bounds = new Rect();
this.oval.roundOut(bounds); ///store our int bounds
//This is what you're looking for ▼
if(!canvas.getClipBounds().contains(bounds)){
if(this.x-size<0 || this.x+size > canvas.getWidth()){
velX=-velX;
}
if(this.y-size<0 || this.y+size > canvas.getHeight()){
velY=-velY;
}
}
}
}
The program works perfecly.
I studied it deeply as good as I could. But after it and after watching the documentation I couldn't understand two thing:
Where and when the method onDraw(Canvas canvas) is called the first time.
Why at the end of onDraw there is invalidate()?
I mean the documentation said :
Invalidate the whole view. If the view is visible, onDraw(android.graphics.Canvas) will be called at some point in the future.
so... if this method is used to call onDraw,why don't call it direcly? what's the difference?
1)The onDraw method will be called by the framework, whenever the view is invalid. A view is invalid when it first comes on screen, so when you set your content view for an activity they layout and all views in it will be measured, laid out, then drawn (via onDraw).
After that the UI thread will call onDraw if needed every 16ms or so (so it draws at 60 FPS).
2)Its marking the view as needing to be redrawn, so the next time the the screen is drawn onDraw will be called. Otherwise it would be skipped, as we could assume it isn't needed.
Why you don't call onDraw directly- efficiency. In a very simple drawing system you would- but drawing is time consuming, you don't want to do it more than you have to. So instead of drawing immediately (which wouldn't work anyway, you wouldn't have the right Canvas to pass to onDraw), you call invalidate and the system will call onDraw if needed at a regular interval.
Note that this isn't particularly good code. In particular, having the onDraw trigger the move which updates the balls location instead of using a timer is icky. Having onDraw call invalidate as a result is also kind of icky. A better solution would be to separate the view, model, and timer into more of an MVC or MVP system.

Canvas DrawLine Is Invisible

I am trying to draw a single line in Android using canvas
My class :
public class LineDrawer extends View {
public LineDrawer(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(10);
float left = 20;
float top = 20;
float right = 50;
float bottom = 100;
canvas.drawLine(left, top, right, bottom, paint);
}
}
My Main Activity :
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LineDrawer lineDrawer = new LineDrawer(this);
setContentView(R.layout.activity_Main);
}
}
I cannot find where is the problem , I try all the solutions in the internet but nothing happen , still a blank activity..
Should I import some code ?
lineDrawer is created but not added anywhere. Just creating a view is not enough, you need to add it to the current displayed views to be taken into account and drawn. You have two options:
Add it to your XML layout. You will have to add the following constructor to your custom view.
public LineDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
}
Use addView(). Anyway, given how simple is your example, I'll use first (common) method.
As an additional comment, the Paint paint object should be created on view initialization, as is a costly operation. See in the original documentation for more information about this.

How to make a linear layout scrollable and zoomable in Android?

I have a LinearLayout in my Android app and I would like to make it scrollable in all directions. I simply implemented this with a HorizontalScrollView and ScrollView under which I placed my layout.
This works great, but I would like to implement pinch-zoom as well.
So I was inspired by this question: Android - zoom in/out RelativeLayout with spread/pinch
and I came up with the following layout:
<com.example.ZoomableScrollView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.2"
android:scrollbars="none"
android:background="#android:color/black">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:id="#+id/sheet_layout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
</LinearLayout>
</HorizontalScrollView>
</com.example.ZoomableScrollView>
The children of the inner LinearLayout are dynamically added (they are simple TextViews inside horizontal LinearLayouts to make a simple table).
The ZoomableScrollView is quite simple and looks like this:
public class ZoomableScrollView extends ScrollView {
float mScaleFactor = 1.f;
private final ScaleGestureDetector mScaleDetector;
static final float MIN_SCALE = 1f;
static final float MAX_SCALE = 4f;
public ZoomableScrollView(Context context) {
this(context, null, 0);
}
public ZoomableScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZoomableScrollView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
mScaleDetector = new ScaleGestureDetector(context, new OnScaleListener());
}
#Override
public boolean onTouchEvent(MotionEvent event) {
Log.w("DEBUG", "On touch event");
boolean retVal = mScaleDetector.onTouchEvent(event);
return super.onTouchEvent(event) || retVal;
}
#Override
protected void dispatchDraw(Canvas canvas) {
Log.w("DEBUG", String.format("Scaling with scale factor %f", mScaleFactor));
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScaleFactor, mScaleFactor);
super.dispatchDraw(canvas);
canvas.restore();
}
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
private class OnScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
{
#Override
public boolean onScale(ScaleGestureDetector detector)
{
mScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(MIN_SCALE, Math.min(mScaleFactor, MAX_SCALE));
Log.w("DEBUG", "OnScale: scale factor: " + mScaleFactor);
invalidate();
return true;
}
}
}
I have 2 issues with this:
The zooming is not very smooth, sometimes the View will scroll when it should zoom. That's not such a big deal atm.
When zooming, the LinearLayout becomes smaller and you can't scroll everywhere anymore. I think I understand why this is the case, but I don't know how to change that and I'm thinking that I went into the wrong direction here.
I don't mind changing the layout completely. In the end, I would like to have it behave a bit like a browser, where you can zoom and scroll everywhere (not as complex though). Is there a simple solution for this ?
As for two dimensional scrollView I had used this one. Maybe you should try to add pinch for it.

custom made arc shaped seekbar

What I am trying to achieve is to make an arc shaped seekbar. I know there are plenty of libraries I could use to achieve this, but I am just trying my hands on custom made views.
I have encountered couple of problems:
I have a class which extends SeekBar, and I have implemented onDraw and onMeasure methods as well, but I am not able to view that in layout editor in eclipse, here is the code for the custom view class:
package com.custom.android.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.SeekBar;
import android.widget.Toast;
public class CustomSeekBar extends SeekBar {
public CustomSeekBar(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public CustomSeekBar(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CustomSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public void draw(Canvas canvas) {
// TODO Auto-generated method stub
super.draw(canvas);
}
#Override
protected synchronized void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
Here is my layout xml :
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<com.custom.android.views.CustomSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/seekBar"/>
</RelativeLayout>
If I use canvas class to draw an arc or any shape, would that be a good starting point?
What exactly is wrong with the eclipse adt and how could I use the onDraw method to give shape to that seekbar?
Drawing a ProgressBar with any shape, is pretty easy. With the SeekBar you have some complexity, since you have to achieve 3 diferent things:
Draw the line
Draw the draggable thumb, if you want.
Handle the user interaction
You have to think of it as an arc that is draw inside a rectangle. So point 3 could be easy: just let the user move the finger in a horizontal line, or exactly over the arc, but considering only the x coordinate of the touch event. What does this mean, in short? ok, good news: you dont have to do anything, since thats the normal behavior of the base SeekBar.
For the second point, you can choose an image for the handler, and write it in the corresponding position with a little maths. Or you can forget the handler for know, and just draw the seek bar as a line representing the full track, and another line over it representing the progress. When you have this working, if you want you can add the handler.
And for the first point, this is the main one, but its not hard to achieve. You can use this code:
UPDATE: I made some improvements in the code
public class ArcSeekBar extends SeekBar {
public ArcSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ArcSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private Paint mBasePaint;
private Paint mProgressPaint;
private RectF mOval = new RectF(5, 5, 550, 550);
private int defaultmax = 180;
private int startAngle=180;
private int strokeWidth=10;
private int trackColor=0xFF000000;
private int progressColor=0xFFFF0000;
public void setOval(RectF mOval) {
this.mOval = mOval;
}
public void setStartAngle(int startAngle) {
this.startAngle = startAngle;
}
public void setStrokeWidth(int strokeWidth) {
this.strokeWidth = strokeWidth;
}
public void setTrackColor(int trackColor) {
this.trackColor = trackColor;
}
public void setProgressColor(int progressColor) {
this.progressColor = progressColor;
}
public ArcSeekBar(Context context) {
super(context);
mBasePaint = new Paint();
mBasePaint.setAntiAlias(true);
mBasePaint.setColor(trackColor);
mBasePaint.setStrokeWidth(strokeWidth);
mBasePaint.setStyle(Paint.Style.STROKE);
mProgressPaint = new Paint();
mProgressPaint.setAntiAlias(true);
mProgressPaint.setColor(progressColor);
mProgressPaint.setStrokeWidth(strokeWidth);
mProgressPaint.setStyle(Paint.Style.STROKE);
setMax(defaultmax);// degrees
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawArc(mOval, startAngle, getMax(), false, mBasePaint);
canvas.drawArc(mOval, startAngle, getProgress(), false, mProgressPaint);
invalidate();
//Log.i("ARC", getProgress()+"/"+getMax());
}
}
Of course, you can and you should make everything configurable, be means of the contructor, or with some setters for the start and end angles, dimensions of the containing rectangle, stroke widths, colors, etc.
Also, note that the arc is drawn from 0 to getProgress, being this number an angle relative to the x axis, growing clocwise, so, if it go from 0 to 90 degrees, it will be something like:
Of course you can change this: canvas.drawArc get any number as an angle, and it is NOT treated as module 360, but you can do the maths and have it starting and ending in any point you want.
In my example the beggining is in the 9 of a clock, and it takes 180 degrees, to the 3 in the clock.
UPDATE
I uploaded a running example to github

Drawing over a view and all it's children

I'm trying to apply a visual effect to a viewgroup. My idea is to grab a bitmap of the viewgroup, shrink it down, expand it back up, and draw it over the viewgroup to give it a blocky, low quality effect.
I've got most of the way there using this code:
public class Blocker {
private static final float RESAMPLE_QUALITY = 0.66f; // less than 1, lower = worse quality
public static void block(Canvas canvas, Bitmap bitmap_old) {
block(canvas, bitmap_old, RESAMPLE_QUALITY);
}
public static void block(Canvas canvas, Bitmap bitmap_old, float quality) {
Bitmap bitmap_new = Bitmap.createScaledBitmap(bitmap_old, Math.round(bitmap_old.getWidth() * RESAMPLE_QUALITY), Math.round(bitmap_old.getHeight() * RESAMPLE_QUALITY), true);
Rect from = new Rect(0, 0, bitmap_new.getWidth(), bitmap_new.getHeight());
RectF to = new RectF(0, 0, bitmap_old.getWidth(), bitmap_old.getHeight());
canvas.drawBitmap(bitmap_new, from, to, null);
}
}
I simply pass in the canvas to draw on and a bitmap of what needs to be scaled down+up and it works well.
public class BlockedLinearLayout extends LinearLayout {
private static final String TAG = BlockedLinearLayout.class.getSimpleName();
public BlockedLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
applyCustomAttributes(context, attrs);
setup();
}
public BlockedLinearLayout(Context context) {
super(context);
setup();
}
private void setup() {
this.setDrawingCacheEnabled(true);
}
#Override
public void draw(Canvas canvas) {
super.draw(canvas);
// block(canvas); If I call this here, it works but no updates
}
#Override
public void onDraw(Canvas canvas) {
// block(canvas); If I call this here, draws behind children, still no updates
}
private void block(Canvas canvas) {
Blocker.block(canvas, this.getDrawingCache());
}
}
The problem I'm having is in my viewgroup. If I run the block method in the viewgroup's draw, it draws over everything but doesn't ever update when child views change. I've traced function calls with Log, and the draw method seems to be running, but nothing changes.
I've also tried implementing this in onDraw. This draws the bitmap behind all the children views, and again they aren't updating.
Can anyone explain how I would go about fixing this?
Try this:
#Override
protected void dispatchDraw(Canvas canvas) {
// call block() here if you want to draw behind children
super.dispatchDraw(canvas);
// call block() here if you want to draw over children
}
And call destroyDrawingCache() and then, buildDrawingCache() each time you change a child.
Draw() method will work well for you.
I'm now trying to make a count time view in a circle shape, when time is passing, the view will reducing his angle. It's used to cover profile photo(a circle shape photo).
Starting with Android API 23, you can use onDrawForeground(Canvas) to draw on top of child views: https://developer.android.com/reference/android/view/View#onDrawForeground(android.graphics.Canvas)
Unlike onDraw() though, be sure to call through to the super class:
#Override
public void onDrawForeground(final Canvas canvas) {
super.onDrawForeground(canvas);
// Your code here
}

Categories

Resources