Showing posts with label Custom View. Show all posts
Showing posts with label Custom View. Show all posts

Wednesday, August 10, 2016

Custom view to draw bitmap along path, calculate in background thread

Last post show a example of "Custom view to draw bitmap along path", with calculation run inside onDraw(). It's modified version to pre-calculate in back thread.


(remark: in last post, canvas.drawPath() is called inside onDraw(). It seem run very slow, removed in this example.)

Modify AnimationView.java
package com.blogspot.android_er.androidmovingbitmapalongpath;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

public class AnimationView extends View {

    List<AnimationThing> animationThingsList;
    public AnimationView(Context context) {
        super(context);
        initAnimationView();
    }

    public AnimationView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAnimationView();
    }

    public AnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAnimationView();
    }

    private void initAnimationView(){
        animationThingsList = new ArrayList<>();
    }

    public void insertThing(AnimationThing thing){
        animationThingsList.add(thing);
    }

    //prepare calculation in background thread
    public void preDraw(){
        for (AnimationThing thing : animationThingsList){
            if(thing.distance < thing.pathLength){
                thing.pathMeasure.getPosTan(thing.distance, thing.pos, thing.tan);

                thing.matrix.reset();
                float degrees = (float)(Math.atan2(thing.tan[1], 
                        thing.tan[0])*180.0/Math.PI);
                thing.matrix.postRotate(degrees, thing.bm_offsetX, thing.bm_offsetY);
                thing.matrix.postTranslate(thing.pos[0]-thing.bm_offsetX, 
                        thing.pos[1]-thing.bm_offsetY);

                thing.distance += thing.step;
            }else{
                thing.distance = 0;
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (AnimationThing thing : animationThingsList){
            canvas.drawBitmap(thing.bm, thing.matrix, null);
        }

        invalidate();
    }

    /*
    @Override
    protected void onDraw(Canvas canvas) {
        for (AnimationThing thing : animationThingsList){

            //This code run slow!!!
            //canvas.drawPath(thing.animPath, thing.paint);

            if(thing.distance < thing.pathLength){
                thing.pathMeasure.getPosTan(thing.distance, thing.pos, thing.tan);

                thing.matrix.reset();
                float degrees = (float)(Math.atan2(thing.tan[1], 
                    thing.tan[0])*180.0/Math.PI);
                thing.matrix.postRotate(degrees, thing.bm_offsetX, thing.bm_offsetY);
                thing.matrix.postTranslate(thing.pos[0]-thing.bm_offsetX, 
                    thing.pos[1]-thing.bm_offsetY);

                canvas.drawBitmap(thing.bm, thing.matrix, null);

                thing.distance += thing.step;
            }else{
                thing.distance = 0;
            }
        }

        invalidate();
    }
    */


}


MainActivity.java
package com.blogspot.android_er.androidmovingbitmapalongpath;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Path;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    AnimationView myAnimationView;
    AniThread myAniThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myAnimationView = (AnimationView)findViewById(R.id.MyAnimationView);

        prepareThings();
        myAniThread = new AniThread(myAnimationView);
        myAniThread.start();
    }

    private void prepareThings() {
        Path animPath;
        float step;
        Bitmap bm;

        bm = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);

        animPath = new Path();
        animPath.moveTo(100, 100);
        animPath.lineTo(200, 100);
        animPath.lineTo(300, 50);
        animPath.lineTo(400, 150);
        animPath.lineTo(100, 300);
        animPath.lineTo(600, 300);
        animPath.lineTo(100, 100);
        animPath.close();

        step = 1;

        AnimationThing thing = new AnimationThing(animPath, bm, step);
        myAnimationView.insertThing(thing);

        //The second thing
        bm = BitmapFactory.decodeResource(getResources(), android.R.drawable.ic_menu_add);

        animPath.reset();
        animPath.addCircle(400, 400, 300, Path.Direction.CW);
        step = 3;
        thing = new AnimationThing(animPath, bm, step);
        myAnimationView.insertThing(thing);
    }

    class AniThread extends Thread{

        AnimationView targetView;

        public AniThread(AnimationView target) {
            super();
            targetView = target;
        }

        @Override
        public void run() {
            while (true){
                targetView.preDraw();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

}


The other files, AnimationThing.java and activity_main.xml, refer last post.


download filesDownload the files .

Tuesday, August 9, 2016

Custom view to draw bitmap along path


Refer to my old exercise "Animation of moving bitmap along path", the bitmap and path are hard-coded inside custom view. It's a modified version; the thing to be drawn (bitmap/path) are held separated and referenced by custom view in a list, such that it is easy to insert more things.


Please notice:
- I don't thing it's a good approach to do this, I just show a interesting exercise.
- You should not do the calculation (such as move the bitmap) inside onDraw(), it's suggested to do it in another thread.

Create a new class AnimationThing.java. It hold the bitmap and path of individual thing.
package com.blogspot.android_er.androidmovingbitmapalongpath;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;

public class AnimationThing {

    Paint paint;
    Path animPath;
    PathMeasure pathMeasure;
    float pathLength;
    Bitmap bm;
    int bm_offsetX, bm_offsetY;
    float step;
    float distance;
    float[] pos;
    float[] tan;
    Matrix matrix;

    public AnimationThing(Paint paint,
                          Path animPath,
                          Bitmap bm,
                          float step) {
        this.paint = paint;

        this.animPath = animPath;
        pathMeasure = new PathMeasure(this.animPath, false);
        pathLength = pathMeasure.getLength();

        this.bm = bm;
        bm_offsetX = bm.getWidth()/2;
        bm_offsetY = bm.getHeight()/2;

        this.step = step;
        distance = 0;
        pos = new float[2];
        tan = new float[2];

        matrix = new Matrix();
    }
}


Create our custom view, AnimationView .java.
package com.blogspot.android_er.androidmovingbitmapalongpath;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

public class AnimationView extends View {

    List<AnimationThing> animationThingsList;
    public AnimationView(Context context) {
        super(context);
        initAnimationView();
    }

    public AnimationView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAnimationView();
    }

    public AnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAnimationView();
    }

    private void initAnimationView(){
        animationThingsList = new ArrayList<>();
    }

    public void insertThing(AnimationThing thing){
        animationThingsList.add(thing);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (AnimationThing thing : animationThingsList){

            //!!! Only the path of the last thing will be drawn on screen
            canvas.drawPath(thing.animPath, thing.paint);

            if(thing.distance < thing.pathLength){
                thing.pathMeasure.getPosTan(thing.distance, thing.pos, thing.tan);

                thing.matrix.reset();
                float degrees = (float)(Math.atan2(thing.tan[1], thing.tan[0])*180.0/Math.PI);
                thing.matrix.postRotate(degrees, thing.bm_offsetX, thing.bm_offsetY);
                thing.matrix.postTranslate(thing.pos[0]-thing.bm_offsetX, thing.pos[1]-thing.bm_offsetY);

                canvas.drawBitmap(thing.bm, thing.matrix, null);

                thing.distance += thing.step;
            }else{
                thing.distance = 0;
            }
        }

        invalidate();
    }
}


Layout, activity_main.xml.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:padding="16dp"
    android:orientation="vertical"
    tools:context="com.blogspot.android_er.androidmovingbitmapalongpath.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <com.blogspot.android_er.androidmovingbitmapalongpath.AnimationView
        android:id="@+id/MyAnimationView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#F0F0F0"/>
</LinearLayout>


MainActivity.java
package com.blogspot.android_er.androidmovingbitmapalongpath;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    AnimationView myAnimationView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myAnimationView = (AnimationView)findViewById(R.id.MyAnimationView);

        prepareThings();
    }

    private void prepareThings(){
        Paint paint;
        Path animPath;
        float step;
        Bitmap bm;

        paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(1);
        paint.setStyle(Paint.Style.STROKE);

        bm = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);

        animPath = new Path();
        animPath.moveTo(100, 100);
        animPath.lineTo(200, 100);
        animPath.lineTo(300, 50);
        animPath.lineTo(400, 150);
        animPath.lineTo(100, 300);
        animPath.lineTo(600, 300);
        animPath.lineTo(100, 100);
        animPath.close();

        step = 1;

        AnimationThing thing = new AnimationThing(paint, animPath, bm, step);
        myAnimationView.insertThing(thing);

        //The second thing
        bm = BitmapFactory.decodeResource(getResources(), android.R.drawable.ic_menu_add);

        animPath.reset();
        animPath.addCircle(400, 400, 300, Path.Direction.CW);
        step = 3;
        thing = new AnimationThing(paint, animPath, bm, step);
        myAnimationView.insertThing(thing);
    }
}



download filesDownload the files .

Next:
Custom view to draw bitmap along path, calculate in background thread

Tuesday, October 20, 2015

Animated GIF (Androidify) for 3D Hologram viewer


Last post I tried to generate interactive animation using ObjectAnimator, to simulate 3D Hologram effect. This post I create Custom View to display animated GIF of Androidify, to simulate the effect.


The animated GIFs of Androidify are create using Google's Androidify App.  Then I have to edit the animated GIFs to resize it to 200x200, and change the background to black using GIMP. Then copy the files to drawable folder in Android Studio project.

Create a custom view, GifView, to handle the animated GIF. If you are looking for display animated GIF, refer "Play animated GIF using android.graphics.Movie, with Movie.decodeStream(InputStream)". In this version, I have to handle src attribute from xml, such that I can display four GifView of different source.

GifView .java
package com.blogspot.android_er.androidmirror;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.util.AttributeSet;
import android.view.View;

import java.io.InputStream;

public class GifView extends View {

    private InputStream gifInputStream;
    private Movie gifMovie;
    private int movieWidth, movieHeight;
    private long movieDuration;
    private long mMovieStart;

    public GifView(Context context) {
        super(context);
        init(context, null);
    }

    public GifView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public GifView(Context context, AttributeSet attrs,
                   int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs){
        setFocusable(true);

        //Handle src attribute from xml
        if(attrs == null){
            gifInputStream = context.getResources().openRawResource(R.drawable.androidify1b);
        }else{
            int src_id = attrs.getAttributeResourceValue(
                    "http://schemas.android.com/apk/res/android",
                    "src",
                    R.drawable.androidify1b);
            gifInputStream = context.getResources().openRawResource(src_id);
        }

        gifMovie = Movie.decodeStream(gifInputStream);
        movieWidth = gifMovie.width();
        movieHeight = gifMovie.height();
        movieDuration = gifMovie.duration();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec,
                             int heightMeasureSpec) {
        setMeasuredDimension(movieWidth, movieHeight);
    }

    public int getMovieWidth(){
        return movieWidth;
    }

    public int getMovieHeight(){
        return movieHeight;
    }

    public long getMovieDuration(){
        return movieDuration;
    }

    @Override
    protected void onDraw(Canvas canvas) {

        long now = android.os.SystemClock.uptimeMillis();
        if (mMovieStart == 0) {   // first time
            mMovieStart = now;
        }

        if (gifMovie != null) {

            int dur = gifMovie.duration();
            if (dur == 0) {
                dur = 1000;
            }

            int relTime = (int)((now - mMovieStart) % dur);

            gifMovie.setTime(relTime);

            gifMovie.draw(canvas, 0, 0);
            invalidate();

        }

    }
}


layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:background="@android:color/black"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold"
        android:layout_alignParentTop="true"/>

    <GridLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:columnCount="3"
        android:layout_centerInParent="true">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <com.blogspot.android_er.androidmirror.GifView
            android:id="@+id/image2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/androidify1b"
            android:layout_gravity="center"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <com.blogspot.android_er.androidmirror.GifView
            android:id="@+id/image4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/androidify2b"
            android:layout_gravity="center"
            android:rotation="270"/>
        <ImageView
            android:layout_width="20mm"
            android:layout_height="20mm"/>
        <com.blogspot.android_er.androidmirror.GifView
            android:id="@+id/image6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/androidify3b"
            android:layout_gravity="center"
            android:rotation="90"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <com.blogspot.android_er.androidmirror.GifView
            android:id="@+id/image8"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/androidify4b"
            android:layout_gravity="center"
            android:rotation="180"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </GridLayout>
</RelativeLayout>


Edit AndroidManifest.xml to disable hardwareAccelerated by setting it false.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.blogspot.android_er.androidmirror" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity android:name=".MainActivity"
            android:hardwareAccelerated="false">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


Nothing do on MainActivity.java
package com.blogspot.android_er.androidmirror;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

}



download filesDownload the files (Android Studio Format) .

download filesDownload APK .


If you have a DIY 3D Hologram Projector, you can view the test view at YouTube.

Saturday, September 12, 2015

Create custom color button, in Android Studio


To create custom buton, create a XML file under drawable folder.


drawable/colorbutton.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <gradient
                android:angle="90"
                android:endColor="#000050"
                android:startColor="#0000E0" />
            <stroke
                android:width="3dp"
                android:color="#303030" />
            <corners
                android:radius="10dp" />
            <padding
                android:bottom="10dp"
                android:left="10dp"
                android:right="10dp"
                android:top="10dp" />
        </shape>
    </item>

    <item android:state_focused="true">
        <shape>
            <gradient
                android:angle="270"
                android:endColor="#500000"
                android:startColor="#E00000" />
            <stroke
                android:width="3dp"
                android:color="#303030" />
            <corners
                android:radius="10dp" />
            <padding
                android:bottom="10dp"
                android:left="10dp"
                android:right="10dp"
                android:top="10dp" />
        </shape>
    </item>

    <item>
        <shape>
            <gradient
                android:angle="270"
                android:endColor="#005050"
                android:startColor="#00E0E0" />
            <stroke
                android:width="3dp"
                android:color="#303030" />
            <corners
                android:radius="10dp" />
            <padding
                android:bottom="10dp"
                android:left="10dp"
                android:right="10dp"
                android:top="10dp" />
        </shape>
    </item>
</selector>

Use it in layout file, layout/activity_main.xml
<LinearLayout 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:orientation="vertical"
    android:padding="10dp"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Color Button"
        android:background="@drawable/colorbutton"
        android:textColor="@android:color/white"/>

</LinearLayout>



How to do it in Android Studio:




Related:
- Android pre-defined Button style - buttonStyle, borderlessButtonStyle, buttonStyleToggle and buttonStyleInset

Wednesday, May 27, 2015

Android custom touch view, with callback interface.

This example, implement custom view with touch function, and also callback interface. And also implement listener at activity side. Such that the view can pass touched information to activity.


com.example.androidtouchview.TouchView.java, custom view.
package com.example.androidtouchview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class TouchView extends View {

    private Paint paint, touchPaint;
    private boolean touched;
    private float touchX, touchY;
    private float touchMajor, touchMinor;

    public TouchView(Context context) {
        super(context);
        init(null, 0);
    }

    public TouchView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public TouchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs, defStyleAttr);
    }

    private void init(AttributeSet attrs, int defStyle) {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(1);

        touchPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        touchPaint.setColor(Color.RED);
        touchPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        touchPaint.setStrokeWidth(1);

        touched = false;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        canvas.drawRect(
                0,
                0,
                getWidth(),
                getHeight(),
                paint);
        canvas.drawRect(
                paddingLeft,
                paddingTop,
                getWidth()- paddingRight,
                getHeight()- paddingBottom,
                paint);

        if(touched){
            canvas.drawCircle(touchX, touchY, touchMinor, touchPaint);
            canvas.drawCircle(touchX, touchY, touchMajor, paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()){
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_DOWN:
                touchX = event.getX();
                touchY = event.getY();
                touchMajor = event.getTouchMajor();
                touchMinor = event.getTouchMinor();
                touched = true;
                break;
            default:
                touched = false;
        }

        onViewTouchedListener.OnViewTouched(touchX, touchY, touched);

        invalidate();
        return true;
    }

    /*
    Set up callback function
     */
    private OnViewTouchedListener onViewTouchedListener;
    public interface OnViewTouchedListener {
        public void OnViewTouched(float x, float y, boolean touched);
    }

    public void setOnViewTouchedListener(OnViewTouchedListener listener) {
        onViewTouchedListener = listener;
    }
}


layout/activity_main.xml
<LinearLayout 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:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/touchedInfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#E0E0E0">
        <com.example.androidtouchview.TouchView
            android:id="@+id/myTouchView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingLeft="50dp"
            android:paddingRight="50dp"
            android:paddingTop="50dp"
            android:paddingBottom="50dp" />
    </LinearLayout>


</LinearLayout>

com.example.androidtouchview.MainActivity
package com.example.androidtouchview;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.TextView;

public class MainActivity extends ActionBarActivity {

    TextView touchedInfo;
    TouchView touchView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        touchedInfo = (TextView)findViewById(R.id.touchedInfo);
        touchView = (TouchView)findViewById(R.id.myTouchView);
        touchView.setOnViewTouchedListener(new TouchView.OnViewTouchedListener() {
            @Override
            public void OnViewTouched(float x, float y, boolean touched) {
                touchedInfo.setText(
                        "Touched: " + touched + "\n" +
                        "x: " + x + "\n" + "y: " + y);
            }
        });

    }

}



download filesDownload the files (Android Studio Format).

Create custom view with touch detection

Here we implement a custom view with touch detection, to show touched area.


com.example.androidtouchview.TouchView.java, our custom view.
package com.example.androidtouchview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class TouchView extends View {

    private Paint paint, touchPaint;
    private boolean touched;
    private float touchX, touchY;
    private float touchMajor, touchMinor;

    public TouchView(Context context) {
        super(context);
        init(null, 0);
    }

    public TouchView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public TouchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs, defStyleAttr);
    }

    private void init(AttributeSet attrs, int defStyle) {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(1);

        touchPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        touchPaint.setColor(Color.RED);
        touchPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        touchPaint.setStrokeWidth(1);

        touched = false;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        canvas.drawRect(
                0,
                0,
                getWidth(),
                getHeight(),
                paint);
        canvas.drawRect(
                paddingLeft,
                paddingTop,
                getWidth()- paddingRight,
                getHeight()- paddingBottom,
                paint);

        if(touched){
            canvas.drawCircle(touchX, touchY, touchMinor, touchPaint);
            canvas.drawCircle(touchX, touchY, touchMajor, paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()){
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_DOWN:
                touchX = event.getX();
                touchY = event.getY();
                touchMajor = event.getTouchMajor();
                touchMinor = event.getTouchMinor();
                touched = true;
                break;
            default:
                touched = false;
        }
        invalidate();
        return true;
    }
}


layout/activity_main.xml
<LinearLayout 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:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#E0E0E0">
        <com.example.androidtouchview.TouchView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingLeft="50dp"
            android:paddingRight="50dp"
            android:paddingTop="50dp"
            android:paddingBottom="50dp" />
    </LinearLayout>


</LinearLayout>


download filesDownload the files (Android Studio Format).

Android custom touch view, with callback interface.

Wednesday, April 8, 2015

Implement custom View to display Animated GIF

It's a improved version of the example "Play animated GIF using android.graphics.Movie, with Movie.decodeStream(InputStream)".


- Add function of Run/Stop, and Repeat setting
- Instead of disable hardware acceleration in AndroidManifest.xml, the custom AnimatedGifView disable hardware acceleration by calling setLayerType(), for API Level 11.

Our custom view, AnimatedGifView.gif
package com.example.androidgif;

import java.io.InputStream;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.util.AttributeSet;
import android.view.View;

public class AnimatedGifView extends View {

 private InputStream gifInputStream;
 private Movie gifMovie;
 private int movieWidth, movieHeight;
 private long movieDuration;
 private long movieRunDuration;
 private long lastTick;
 private long nowTick;

 private boolean repeat = true;
 private boolean running = true;

 public void setRepeat(boolean r) {
  repeat = r;
 }

 public void setRunning(boolean r) {
  running = r;
 }

 public AnimatedGifView(Context context) {
  super(context);
  init(context);
 }

 public AnimatedGifView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }

 public AnimatedGifView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init(context);
 }

 private void init(Context context) {

  // Turn OFF hardware acceleration
  // API Level 11
  setLayerType(View.LAYER_TYPE_SOFTWARE, null);

  setFocusable(true);
  gifInputStream = context.getResources().openRawResource(
    R.drawable.android_er);

  gifMovie = Movie.decodeStream(gifInputStream);
  movieWidth = gifMovie.width();
  movieHeight = gifMovie.height();
  movieDuration = gifMovie.duration();
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(movieWidth, movieHeight);
 }

 public int getMovieWidth() {
  return movieWidth;
 }

 public int getMovieHeight() {
  return movieHeight;
 }

 public long getMovieDuration() {
  return movieDuration;
 }

 @Override
 protected void onDraw(Canvas canvas) {
  
  if(gifMovie == null){
   return;
  }

  nowTick = android.os.SystemClock.uptimeMillis();
  if (lastTick == 0) { // first time
   movieRunDuration = 0;
  }else{
   if(running){
    movieRunDuration += nowTick-lastTick;
    if(movieRunDuration > movieDuration){
     if(repeat){
      movieRunDuration = 0;
     }else{
      movieRunDuration = movieDuration;
     }
    }
   }
  }
  
  gifMovie.setTime((int)movieRunDuration);
  gifMovie.draw(canvas, 0, 0);
  
  lastTick = nowTick;
  invalidate();

 }
}

MainActivity.java
package com.example.androidgif;

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.TextView;
import android.widget.ToggleButton;

public class MainActivity extends ActionBarActivity {

 TextView textViewInfo;
 AnimatedGifView gifView;
 
 CheckBox cbRepeat;
 ToggleButton tbRun;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  gifView = (AnimatedGifView) findViewById(R.id.gifview);
  textViewInfo = (TextView) findViewById(R.id.textinfo);

  String stringInfo = "";
  stringInfo += "Duration: " + gifView.getMovieDuration() + "\n";
  stringInfo += "W x H: " + gifView.getMovieWidth() + " x "
    + gifView.getMovieHeight() + "\n";

  textViewInfo.setText(stringInfo);
  
  cbRepeat = (CheckBox)findViewById(R.id.repeat);
  cbRepeat.setOnCheckedChangeListener(new OnCheckedChangeListener(){

   @Override
   public void onCheckedChanged(CompoundButton buttonView,
     boolean isChecked) {
    gifView.setRepeat(isChecked);
   }});
  
  tbRun = (ToggleButton)findViewById(R.id.run);
  tbRun.setOnCheckedChangeListener(new OnCheckedChangeListener(){

   @Override
   public void onCheckedChanged(CompoundButton buttonView,
     boolean isChecked) {
    gifView.setRunning(isChecked);
   }});
 }

}

activity_main.xml
<LinearLayout 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:orientation="vertical"
    tools:context="com.example.androidgif.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />
    
    <CheckBox
        android:id="@+id/repeat"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Repeat"
        android:checked="true"/>
    <ToggleButton
        android:id="@+id/run"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textOn="Stop"
        android:textOff="Run"
        android:checked="true"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/android_er"
            />

        <com.example.androidgif.AnimatedGifView
            android:id="@+id/gifview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>

    <TextView
        android:id="@+id/textinfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="info..." />

</LinearLayout>



download filesDownload the files.

Friday, September 5, 2014

Make square shaped layout, equal width and height

To make a layout in square shape, with equal width and height, we can re-assign LayoutParams.width and height of our layout in OnGlobalLayoutListener.

like this:
  myGridLayout.getViewTreeObserver().addOnGlobalLayoutListener(
   new OnGlobalLayoutListener(){

   @Override
   public void onGlobalLayout() {
    
    int pLength;
    int pWidth = myGridLayout.getWidth();
    int pHeight = myGridLayout.getHeight();
    
    //Set myGridLayout equal width and height
    if(pWidth>=pHeight){
     pLength = pHeight;
    }else{
     pLength = pWidth;
    }
    ViewGroup.LayoutParams pParams = myGridLayout.getLayoutParams();
    pParams.width = pLength;
    pParams.height = pLength;
    myGridLayout.setLayoutParams(pParams);
    
    //deprecated in API level 16
    myGridLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    //for API Level >= 16
    //myGridLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
   }});


Update last exercise.

Modify activity_main.xml, to change 8x8 centered GridLayout
<LinearLayout 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:orientation="vertical"
    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="com.example.androidtouchview.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <GridLayout
        android:id="@+id/mygrid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:columnCount="8"
        android:rowCount="8"
        android:layout_gravity="center"
        android:background="@android:color/background_light" >
    </GridLayout>

</LinearLayout>

Modify MainActivity.java
package com.example.androidtouchview;

import com.example.androidtouchview.MyView.OnToggledListener;

import android.support.v7.app.ActionBarActivity;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.GridLayout;
import android.widget.Toast;
import android.os.Bundle;

public class MainActivity extends ActionBarActivity 
 implements OnToggledListener{
 
 int numberOfGlobalLayoutCalled = 0;
 
 MyView[] myViews;
 
 GridLayout myGridLayout;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  myGridLayout = (GridLayout)findViewById(R.id.mygrid);
  
  int numOfCol = myGridLayout.getColumnCount();
  int numOfRow = myGridLayout.getRowCount();
  myViews = new MyView[numOfCol*numOfRow];
  for(int yPos=0; yPos<numOfRow; yPos++){
   for(int xPos=0; xPos<numOfCol; xPos++){
    MyView tView = new MyView(this, xPos, yPos);
    tView.setOnToggledListener(this);
    myViews[yPos*numOfCol + xPos] = tView;
    myGridLayout.addView(tView);
   }
  }
  
  myGridLayout.getViewTreeObserver().addOnGlobalLayoutListener(
   new OnGlobalLayoutListener(){

   @Override
   public void onGlobalLayout() {
    
    int pLength;
    final int MARGIN = 5;
    
    int pWidth = myGridLayout.getWidth();
    int pHeight = myGridLayout.getHeight();
    int numOfCol = myGridLayout.getColumnCount();
    int numOfRow = myGridLayout.getRowCount();
    
    //Set myGridLayout equal width and height
    if(pWidth>=pHeight){
     pLength = pHeight;
    }else{
     pLength = pWidth;
    }
    ViewGroup.LayoutParams pParams = myGridLayout.getLayoutParams();
    pParams.width = pLength;
    pParams.height = pLength;
    myGridLayout.setLayoutParams(pParams);
    
    int w = pLength/numOfCol; //pWidth/numOfCol;
    int h = pLength/numOfRow; //pHeight/numOfRow;
    
    for(int yPos=0; yPos<numOfRow; yPos++){
     for(int xPos=0; xPos<numOfCol; xPos++){
      GridLayout.LayoutParams params = 
       (GridLayout.LayoutParams)myViews[yPos*numOfCol + xPos].getLayoutParams();
      params.width = w - 2*MARGIN;
      params.height = h - 2*MARGIN;
      params.setMargins(MARGIN, MARGIN, MARGIN, MARGIN);
      myViews[yPos*numOfCol + xPos].setLayoutParams(params);
     }
    }
    
    Toast.makeText(MainActivity.this, 
     "numberOfGlobalLayoutCalled = " + numberOfGlobalLayoutCalled, 
     Toast.LENGTH_SHORT).show();
    numberOfGlobalLayoutCalled++;
    
    //deprecated in API level 16
    myGridLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    //for API Level >= 16
    //myGridLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
   }});
 }

 @Override
 public void OnToggled(MyView v, boolean touchOn) {

  //get the id string
  String idString = v.getIdX() + ":" + v.getIdY();

  Toast.makeText(MainActivity.this, 
   "Toogled:\n" +
   idString + "\n" +
   touchOn, 
   Toast.LENGTH_SHORT).show();
 }

}

Thursday, September 4, 2014

OnGlobalLayoutListener called repeatly, and remove it

In last post, we re-layout the views when the global layout state within the view tree changes, with OnGlobalLayoutListener. But once re-layout the views, it trigger another layout state change, and repeat and repeat again.


To prevent it, we can call removeGlobalOnLayoutListener() (deprecated in API level 16) or removeOnGlobalLayoutListener() (for API Level >= 16) after layout.

MainActivity.java
package com.example.androidtouchview;

import com.example.androidtouchview.MyView.OnToggledListener;

import android.support.v7.app.ActionBarActivity;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.GridLayout;
import android.widget.Toast;
import android.os.Bundle;

public class MainActivity extends ActionBarActivity 
 implements OnToggledListener{
 
 int numberOfGlobalLayoutCalled = 0;
 
 MyView[] myViews;
 
 GridLayout myGridLayout;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  myGridLayout = (GridLayout)findViewById(R.id.mygrid);
  
  int numOfCol = myGridLayout.getColumnCount();
  int numOfRow = myGridLayout.getRowCount();
  myViews = new MyView[numOfCol*numOfRow];
  for(int yPos=0; yPos<numOfRow; yPos++){
   for(int xPos=0; xPos<numOfCol; xPos++){
    MyView tView = new MyView(this, xPos, yPos);
    tView.setOnToggledListener(this);
    myViews[yPos*numOfCol + xPos] = tView;
    myGridLayout.addView(tView);
   }
  }
  
  myGridLayout.getViewTreeObserver().addOnGlobalLayoutListener(
   new OnGlobalLayoutListener(){

   @Override
   public void onGlobalLayout() {
    
    final int MARGIN = 5;
    
    int pWidth = myGridLayout.getWidth();
    int pHeight = myGridLayout.getHeight();
    int numOfCol = myGridLayout.getColumnCount();
    int numOfRow = myGridLayout.getRowCount();
    int w = pWidth/numOfCol;
    int h = pHeight/numOfRow;
    
    for(int yPos=0; yPos<numOfRow; yPos++){
     for(int xPos=0; xPos<numOfCol; xPos++){
      GridLayout.LayoutParams params = 
       (GridLayout.LayoutParams)myViews[yPos*numOfCol + xPos].getLayoutParams();
      params.width = w - 2*MARGIN;
      params.height = h - 2*MARGIN;
      params.setMargins(MARGIN, MARGIN, MARGIN, MARGIN);
      myViews[yPos*numOfCol + xPos].setLayoutParams(params);
     }
    }
    
    Toast.makeText(MainActivity.this, 
     "numberOfGlobalLayoutCalled = " + numberOfGlobalLayoutCalled, 
     Toast.LENGTH_SHORT).show();
    numberOfGlobalLayoutCalled++;
    
    //deprecated in API level 16
    myGridLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    //for API Level >= 16
    //myGridLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
   }});
 }

 @Override
 public void OnToggled(MyView v, boolean touchOn) {

  //get the id string
  String idString = v.getIdX() + ":" + v.getIdY();

  Toast.makeText(MainActivity.this, 
   "Toogled:\n" +
   idString + "\n" +
   touchOn, 
   Toast.LENGTH_SHORT).show();
 }

}

Insert view to GridLayout dynamically, with even width and height

This example insert our custom view to a GridLayout (Added in API level 14) dynamically in Java code, and set the width of height evenly.


Modify layout, activity_main.xml, to have a GridLayout without child.
<LinearLayout 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:orientation="vertical"
    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="com.example.androidtouchview.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <GridLayout
        android:id="@+id/mygrid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:columnCount="8"
        android:rowCount="5"
        android:background="@android:color/background_light" >
    </GridLayout>

</LinearLayout>

MyView.java
package com.example.androidtouchview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class MyView extends View {
 
 public interface OnToggledListener {
  void OnToggled(MyView v, boolean touchOn);
 }

 boolean touchOn;
 boolean mDownTouch = false;
 private OnToggledListener toggledListener;
 int idX = 0; //default
 int idY = 0; //default

 public MyView(Context context, int x, int y) {
  super(context);
  idX = x;
  idY = y;
  init();
 }
 
 public MyView(Context context) {
  super(context);
  init();
 }

 public MyView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init();
 }

 public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init();
 }

 private void init() {
  touchOn = false;
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
    MeasureSpec.getSize(heightMeasureSpec));
 }

 @Override
 protected void onDraw(Canvas canvas) {
  if (touchOn) {
   canvas.drawColor(Color.RED);
  } else {
   canvas.drawColor(Color.GRAY);
  }
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  super.onTouchEvent(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
             
             touchOn = !touchOn;
       invalidate();
       
       if(toggledListener != null){
        toggledListener.OnToggled(this, touchOn);
       }
             
                mDownTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (mDownTouch) {
                    mDownTouch = false;
                    performClick();
                    return true;
                }
        }
        return false;
 }

 @Override
 public boolean performClick() {
        super.performClick();
        return true;
 }
 
 public void setOnToggledListener(OnToggledListener listener){
  toggledListener = listener;
 }
 
 public int getIdX(){
  return idX;
 }
 
 public int getIdY(){
  return idY;
 }

}

MainActivity.java
Our custom views, MyView, are created dynamically and add to the GridLayout in onCreate(). Then re-align their LayoutParams in OnGlobalLayoutListener(), will be called when the global layout state or the visibility of views within the view tree changes.
package com.example.androidtouchview;

import com.example.androidtouchview.MyView.OnToggledListener;

import android.support.v7.app.ActionBarActivity;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.GridLayout;
import android.widget.Toast;
import android.os.Bundle;

public class MainActivity extends ActionBarActivity 
 implements OnToggledListener{
 
 MyView[] myViews;
 
 GridLayout myGridLayout;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  myGridLayout = (GridLayout)findViewById(R.id.mygrid);
  
  int numOfCol = myGridLayout.getColumnCount();
  int numOfRow = myGridLayout.getRowCount();
  myViews = new MyView[numOfCol*numOfRow];
  for(int yPos=0; yPos<numOfRow; yPos++){
   for(int xPos=0; xPos<numOfCol; xPos++){
    MyView tView = new MyView(this, xPos, yPos);
    tView.setOnToggledListener(this);
    myViews[yPos*numOfCol + xPos] = tView;
    myGridLayout.addView(tView);
   }
  }
  
  myGridLayout.getViewTreeObserver().addOnGlobalLayoutListener(
   new OnGlobalLayoutListener(){

   @Override
   public void onGlobalLayout() {
    
    final int MARGIN = 5;
    
    int pWidth = myGridLayout.getWidth();
    int pHeight = myGridLayout.getHeight();
    int numOfCol = myGridLayout.getColumnCount();
    int numOfRow = myGridLayout.getRowCount();
    int w = pWidth/numOfCol;
    int h = pHeight/numOfRow;
    
    for(int yPos=0; yPos<numOfRow; yPos++){
     for(int xPos=0; xPos<numOfCol; xPos++){
      GridLayout.LayoutParams params = 
       (GridLayout.LayoutParams)myViews[yPos*numOfCol + xPos].getLayoutParams();
      params.width = w - 2*MARGIN;
      params.height = h - 2*MARGIN;
      params.setMargins(MARGIN, MARGIN, MARGIN, MARGIN);
      myViews[yPos*numOfCol + xPos].setLayoutParams(params);
     }
    }

   }});
 }

 @Override
 public void OnToggled(MyView v, boolean touchOn) {

  //get the id string
  String idString = v.getIdX() + ":" + v.getIdY();

  Toast.makeText(MainActivity.this, 
   "Toogled:\n" +
   idString + "\n" +
   touchOn, 
   Toast.LENGTH_SHORT).show();
 }

}

download filesDownload the files.

Notice:
- OnGlobalLayoutListener will be called repeatly this way. To remove it, call removeGlobalOnLayoutListener() or removeOnGlobalLayoutListener(), read next post.



Implement callback function by implementing interface for custom view

Last post "Custom View, detect touch to toggle color" and also "warning: custom view overrides onTouchEvent but not performClick and #onTouchEvent should call #performClick when a click is detected", we have a self-running custom view to detect user touch and toggle color. But the MainActivity don't know what happen on it. To make MainActivity informed when color toggled, we can implement interface for our custom view.


MyView.java
package com.example.androidtouchview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class MyView extends View {
 
 public interface OnToggledListener {
  void OnToggled(MyView v, boolean touchOn);
 }

 boolean touchOn;
 boolean mDownTouch = false;
 private OnToggledListener toggledListener;

 public MyView(Context context) {
  super(context);
  init();
 }

 public MyView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init();
 }

 public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init();
 }

 private void init() {
  touchOn = false;
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
    MeasureSpec.getSize(heightMeasureSpec));
 }

 @Override
 protected void onDraw(Canvas canvas) {
  if (touchOn) {
   canvas.drawColor(Color.RED);
  } else {
   canvas.drawColor(Color.GRAY);
  }
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  super.onTouchEvent(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
             
             touchOn = !touchOn;
       invalidate();
       
       if(toggledListener != null){
        toggledListener.OnToggled(this, touchOn);
       }
             
                mDownTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (mDownTouch) {
                    mDownTouch = false;
                    performClick();
                    return true;
                }
        }
        return false;
 }

 @Override
 public boolean performClick() {
        super.performClick();
        return true;
 }
 
 public void setOnToggledListener(OnToggledListener listener){
  toggledListener = listener;
 }

}

MainActivity.java
package com.example.androidtouchview;

import com.example.androidtouchview.MyView.OnToggledListener;

import android.support.v7.app.ActionBarActivity;
import android.widget.Toast;
import android.os.Bundle;

public class MainActivity extends ActionBarActivity 
 implements OnToggledListener{
 
 MyView myView00, myView01;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  myView00 = (MyView)findViewById(R.id.myview00);
  myView01 = (MyView)findViewById(R.id.myview01);

  myView00.setOnToggledListener(this);
  myView01.setOnToggledListener(this);
 }

 @Override
 public void OnToggled(MyView v, boolean touchOn) {

  //get the id string
  String idString = getResources().getResourceName(v.getId());

  Toast.makeText(MainActivity.this, 
   "Toogled:\n" +
   idString + "\n" +
   touchOn, 
   Toast.LENGTH_SHORT).show();
 }

}

activity_main.xml
<LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.example.androidtouchview.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        
        <com.example.androidtouchview.MyView 
            android:id="@+id/myview00"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_margin="10dp"/>
        <com.example.androidtouchview.MyView 
            android:id="@+id/myview01"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:layout_margin="10dp"/>
        
    </LinearLayout>
    
</LinearLayout>


Wednesday, September 3, 2014

Custom View, detect touch to toggle color

This example implement a custom view, it have only one function, toggle color if user touched.


Create MyView.java
package com.example.androidtouchview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class MyView extends View {

 boolean touchOn;

 public MyView(Context context) {
  super(context);
  init();
 }

 public MyView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init();
 }

 public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init();
 }

 private void init() {
  touchOn = false;
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
    MeasureSpec.getSize(heightMeasureSpec));
 }

 @Override
 protected void onDraw(Canvas canvas) {
  if (touchOn) {
   canvas.drawColor(Color.RED);
  } else {
   canvas.drawColor(Color.GRAY);
  }
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  super.onTouchEvent(event);
  
  int action = event.getAction();
  
  if(action == MotionEvent.ACTION_DOWN){
   touchOn = !touchOn;
   invalidate();
   return true;
  }
  
  return false;
 }

}

Modify /res/layout/activity_main.xml to include the custom view.
<LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.example.androidtouchview.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        
        <com.example.androidtouchview.MyView 
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_margin="10dp"/>
        <com.example.androidtouchview.MyView 
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:layout_margin="10dp"/>
        
    </LinearLayout>
    
</LinearLayout>

Keep using the auto generated MainActivity.java

You will be warned "MyView overrides onTouchEvent but not performClick" on onTouchEvent() method, read next post to fix it.


Tuesday, June 10, 2014

Use CountDownTimer to do something repeatly, second example.

It is another approach to do the same job as the previous example "Use CountDownTimer to do something repeatly, updating custom view". Instead of implementing CountDownTimer inside custom view as self running function of custom view, this example implement CountDownTimer in MainActivity, and handle all the timing functions. The custom view simple display the graphic only.


MainActivity.java
package com.example.androidcountdowntimer;

import android.app.Activity;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity{
 
 CountDownTimer countDownTimer;
  
 int state;
 final static int STATE_IDLE = 0;
 final static int STATE_RED = 1;
 final static int STATE_GREEN = 2;
 final static int STATE_BLUE = 3;

 TextView myState, myCounter;
 Button btnStart;
 
 SpotView mySpotView;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  mySpotView = (SpotView)findViewById(R.id.myspotview);

  myState = (TextView) findViewById(R.id.mystate);
  
  myCounter = (TextView) findViewById(R.id.mycounter);
  btnStart = (Button) findViewById(R.id.start);
  btnStart.setOnClickListener(btnStartOnClickListener);
  
  state = STATE_IDLE;

 }
 
 OnClickListener btnStartOnClickListener = 
  new OnClickListener(){

   @Override
   public void onClick(View v) {
    
    if(countDownTimer!=null){
       countDownTimer.cancel();   
    }
    
    state = STATE_RED;
    
    countDownTimer = new CountDownTimer(3000, 500) {

     @Override
     public void onTick(long millisUntilFinished) {
      myCounter.setText("onTick: " + millisUntilFinished);
     }

     @Override
     public void onFinish() {
      if(state == STATE_RED){
       state = STATE_GREEN;
       mySpotView.setSpots(false, true, false);
       myState.setText("GREEN");
      }else if(state == STATE_GREEN){
       state = STATE_BLUE;
       mySpotView.setSpots(false, false, true);
       myState.setText("BLUE");
      }else if(state == STATE_BLUE){
       state = STATE_RED;
       mySpotView.setSpots(true, false, false);
       myState.setText("RED");
      }
      
      countDownTimer.start(); 
     }
    };
    
    mySpotView.setSpots(true, false, false);
    myState.setText("RED");
    countDownTimer.start();
   }  
 };

}

SpotView.java
package com.example.androidcountdowntimer;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class SpotView extends View{
 
 boolean spotRedOn, spotGreenOn, spotBlueOn;
 
 Paint paintRed, paintGreen, paintBlue;

 public SpotView(Context context) {
  super(context);
  init();
 }

 public SpotView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init();
 }

 public SpotView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init();
 }
 
 private void init(){

  paintRed = new Paint();
  paintRed.setColor(Color.RED);
  paintRed.setStrokeWidth(1);
  paintRed.setStyle(Paint.Style.FILL);
  
  paintGreen = new Paint();
  paintGreen.setColor(Color.GREEN);
  paintGreen.setStrokeWidth(1);
  paintGreen.setStyle(Paint.Style.FILL);
  
  paintBlue = new Paint();
  paintBlue.setColor(Color.BLUE);
  paintBlue.setStrokeWidth(1);
  paintBlue.setStyle(Paint.Style.FILL);
  
  spotRedOn = spotGreenOn = spotBlueOn = false;
 }

 
 public void setSpots(boolean r, boolean g, boolean b){
  spotRedOn = r;
  spotGreenOn = g;
  spotBlueOn = b;
  invalidate();
 }

 @Override
 protected void onDraw(Canvas canvas) {
  
  float w = getWidth();
  float h = getHeight();
  
  if(spotRedOn){
   canvas.drawCircle(w/4, h/2, 100, paintRed);
  }
  
  if(spotGreenOn){
   canvas.drawCircle(w/2, h/2, 100, paintGreen);
  }
  
  if(spotBlueOn){
   canvas.drawCircle(3*w/4, h/2, 100, paintBlue);
  }
 }

}

Keep using the same layout in previous example.

Monday, June 9, 2014

Use CountDownTimer to do something repeatly, updating custom view.

This example show how to change color spots in custom view repeatly with CountDownTimer. We also implement callback interface on MainActivity.java, such that our custom view can callback MainActivity to pass something.

May be it is not a good practice to achieve such a job, just a example to use CountDownTimer.


MainActivity.java
package com.example.androidcountdowntimer;

import com.example.androidcountdowntimer.SpotView.SpotCallBack;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity 
  implements SpotCallBack{

 TextView myState, myCounter;
 Button btnStart;
 
 SpotView mySpotView;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  mySpotView = (SpotView)findViewById(R.id.myspotview);
  mySpotView.setCallback(this);

  myState = (TextView) findViewById(R.id.mystate);
  
  myCounter = (TextView) findViewById(R.id.mycounter);
  btnStart = (Button) findViewById(R.id.start);
  btnStart.setOnClickListener(new OnClickListener() {

   @Override
   public void onClick(View v) {
    mySpotView.startRun();
   }
  });

 }

 @Override
 public void cb_onTick(String msg) {
  myCounter.setText("onTick: " + msg);
 }

 @Override
 public void cb_onFinish(String msg) {
  myState.setText(msg);
 }

}

SpotView.java
package com.example.androidcountdowntimer;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.CountDownTimer;
import android.util.AttributeSet;
import android.view.View;

public class SpotView extends View{
 
 //used to pass back something to MainActivity
 interface SpotCallBack {
  void cb_onTick(String msg);
  void cb_onFinish(String msg); 
 }
 
 SpotCallBack spotCallback;
 
 CountDownTimer countDownTimer;
 
 int state;
 final static int STATE_IDLE = 0;
 final static int STATE_RED = 1;
 final static int STATE_GREEN = 2;
 final static int STATE_BLUE = 3;
 
 Paint paintRed, paintGreen, paintBlue;

 public SpotView(Context context) {
  super(context);
  init();
 }

 public SpotView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init();
 }

 public SpotView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init();
 }
 
 private void init(){
  state = STATE_IDLE;
  
  paintRed = new Paint();
  paintRed.setColor(Color.RED);
  paintRed.setStrokeWidth(1);
  paintRed.setStyle(Paint.Style.FILL);
  
  paintGreen = new Paint();
  paintGreen.setColor(Color.GREEN);
  paintGreen.setStrokeWidth(1);
  paintGreen.setStyle(Paint.Style.FILL);
  
  paintBlue = new Paint();
  paintBlue.setColor(Color.BLUE);
  paintBlue.setStrokeWidth(1);
  paintBlue.setStyle(Paint.Style.FILL);
 }
 
 public void setCallback(SpotCallBack cb){
  spotCallback = cb;
 }
 
 public void startRun(){
  state = STATE_RED;
  
  if(countDownTimer!=null){
   countDownTimer.cancel();
  }
  
  countDownTimer = new CountDownTimer(3000, 500) {
   
   @Override
   public void onTick(long millisUntilFinished) {
    spotCallback.cb_onTick(String.valueOf(millisUntilFinished));
   }
   
   @Override
   public void onFinish() {
    if(state == STATE_RED){
     state = STATE_GREEN;
     spotCallback.cb_onFinish("GREEN");
    }else if(state == STATE_GREEN){
     state = STATE_BLUE;
     spotCallback.cb_onFinish("BLUE");
    }else if(state == STATE_BLUE){
     state = STATE_RED;
     spotCallback.cb_onFinish("RED");
    }
    countDownTimer.start();
    invalidate();
   }
  };
  
  countDownTimer.start();
  spotCallback.cb_onFinish("RED");
  invalidate();
 }

 @Override
 protected void onDraw(Canvas canvas) {
  
  float w = getWidth();
  float h = getHeight();
  
  if(state == STATE_RED){
   canvas.drawCircle(w/4, h/2, 100, paintRed);
  }else if(state == STATE_GREEN){
   canvas.drawCircle(w/2, h/2, 100, paintGreen);
  }else if(state == STATE_BLUE){
   canvas.drawCircle(3*w/4, h/2, 100, paintBlue);
  }
 }

}

/res/layout/activity_main.xml
<LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.example.androidcountdowntimer.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <Button
        android:id="@+id/start"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Start CountDownTimer" />
    <TextView
        android:id="@+id/mystate"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/mycounter"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
    <com.example.androidcountdowntimer.SpotView 
        android:id="@+id/myspotview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>

Next example:
- Another approach to handle CountDownTimer in MainActivity, and the custom view handle display only.

Saturday, June 7, 2014

Animation follow touch path forward and backward

Further modify the example of "Change speed of Animation follow touch path" to add feature of animation direction, forward and backward.


AnimationView.java
package com.example.androidanimationalongpath;

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.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class AnimationView extends View {
 
 Paint paint;
 
 Bitmap bm;
 int bm_offsetX, bm_offsetY;
 
 Path animPath;
 PathMeasure pathMeasure;
 float pathLength;
 
 float step;   //distance each step
 float distance;  //distance moved
 float curX, curY;
  
 float curAngle;  //current angle
 float targetAngle; //target angle
 float stepAngle; //angle each step

 float[] pos;
 float[] tan;
 
 Matrix matrix;
 
 Path touchPath;
 
 static int FORWARD = 1;
 static int BACKWARD = -1;
 int direction;

 public AnimationView(Context context) {
  super(context);
  initMyView();
 }

 public AnimationView(Context context, AttributeSet attrs) {
  super(context, attrs);
  initMyView();
 }

 public AnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  initMyView();
 }
 
 public void initMyView(){
  paint = new Paint();
  paint.setColor(Color.BLUE);
  paint.setStrokeWidth(1);
  paint.setStyle(Paint.Style.STROKE);
    
  bm = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
  bm_offsetX = bm.getWidth()/2;
  bm_offsetY = bm.getHeight()/2;
  
  animPath = new Path();
  
  pos = new float[2];
  tan = new float[2];
  
  matrix = new Matrix();
  
  touchPath = new Path();
  
  step = 1;  //default
  stepAngle = 1; //default
  
  direction = FORWARD; //default
 }

 @Override
 protected void onDraw(Canvas canvas) {
  if(animPath.isEmpty()){
   return;
  }
  
  canvas.drawPath(animPath, paint);
  
  matrix.reset();
  
  if((targetAngle-curAngle)>stepAngle){
   curAngle += stepAngle;
   matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
   matrix.postTranslate(curX, curY);
   canvas.drawBitmap(bm, matrix, null);
   
   invalidate();
  }else if((curAngle-targetAngle)>stepAngle){
   curAngle -= stepAngle;
   matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
   matrix.postTranslate(curX, curY);
   canvas.drawBitmap(bm, matrix, null);
   
   invalidate();
  }else{
   curAngle=targetAngle;
   
   if((direction==FORWARD && distance < pathLength)
    ||(direction==BACKWARD && distance > 0)){
    
    pathMeasure.getPosTan(distance, pos, tan);

    targetAngle = (float)(Math.atan2(tan[1], tan[0])*180.0/Math.PI);
    matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
    
    curX = pos[0]-bm_offsetX;
    curY = pos[1]-bm_offsetY;
    matrix.postTranslate(curX, curY);
    
    canvas.drawBitmap(bm, matrix, null);
    
    distance += step * direction;
    
    invalidate();
   }else{
    matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
    matrix.postTranslate(curX, curY);
    canvas.drawBitmap(bm, matrix, null);
   }
  }

 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  
  int action = event.getAction();
  
  switch(action){
  case MotionEvent.ACTION_DOWN:
   touchPath.reset();
   touchPath.moveTo(event.getX(), event.getY());
   break;
  case MotionEvent.ACTION_MOVE:
   touchPath.lineTo(event.getX(), event.getY());
   break;
  case MotionEvent.ACTION_UP:
   touchPath.lineTo(event.getX(), event.getY());
   animPath = new Path(touchPath);
   
   pathMeasure = new PathMeasure(animPath, false);
   pathLength = pathMeasure.getLength();
   
   //step = 1;
   distance = 0;
   curX = 0;
   curY = 0;
   
   //stepAngle = 1; 
   curAngle = 0;
   targetAngle = 0;
   
   direction = FORWARD;
   
   invalidate();
   
   break;
    
  }
  
  return true;
 }
 
 public void setSpeed(int sp){
  step = sp;
  stepAngle = sp;
 }
 
 public void replayForward(){
  direction = FORWARD;
  invalidate();
 }
 
 public void replayBackward(){
  direction = BACKWARD;
  invalidate();
 }

}

/res/layout/activity_main.xml
<LinearLayout 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:orientation="vertical"
    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="com.example.androidanimationalongpath.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >
        
        <Button 
            android:id="@+id/replayfor"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:text="Replay/Forward" />
        <Button 
            android:id="@+id/replayback"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:text="Replay/Backward" />
        
    </LinearLayout>

    <SeekBar
        android:id="@+id/speedbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="10"
        android:progress="1"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >
        
        <com.example.androidanimationalongpath.AnimationView
            android:id="@+id/animationview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="10dp"
            android:background="@android:color/darker_gray" />

    </LinearLayout>

</LinearLayout>

MainActivity.java
package com.example.androidanimationalongpath;

import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.os.Bundle;

public class MainActivity extends ActionBarActivity {
 
 AnimationView animationView;
 SeekBar speedBar;
 Button btnForward, btnbackward;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  animationView = (AnimationView)findViewById(R.id.animationview);
  speedBar = (SeekBar)findViewById(R.id.speedbar);
  speedBar.setOnSeekBarChangeListener(speedBarOnSeekBarChangeListener);
  animationView.setSpeed(speedBar.getProgress()); //set default speed
  
  btnForward = (Button)findViewById(R.id.replayfor);
  btnForward.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View v) {
    animationView.replayForward();
   }});
  
  btnbackward = (Button)findViewById(R.id.replayback);
  btnbackward.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View v) {
    animationView.replayBackward();
   }});
 }
 
 OnSeekBarChangeListener speedBarOnSeekBarChangeListener = 
  new OnSeekBarChangeListener(){

   @Override
   public void onProgressChanged(SeekBar seekBar, int progress,
     boolean fromUser) {
    //offset from SeekBar 0~19 to step 1~10
    animationView.setSpeed(progress);
   }

   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {}

   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {}
  };

}


download filesDownload the files.

More example of Drawing Path on canvas of custom View.


Friday, June 6, 2014

Change speed of Animation follow touch path

This exercise change the speed of the former example of "Animation follow touch path".


AnimationView.java
package com.example.androidanimationalongpath;

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.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class AnimationView extends View {
 
 Paint paint;
 
 Bitmap bm;
 int bm_offsetX, bm_offsetY;
 
 Path animPath;
 PathMeasure pathMeasure;
 float pathLength;
 
 float step;   //distance each step
 float distance;  //distance moved
 float curX, curY;
  
 float curAngle;  //current angle
 float targetAngle; //target angle
 float stepAngle; //angle each step

 float[] pos;
 float[] tan;
 
 Matrix matrix;
 
 Path touchPath;

 public AnimationView(Context context) {
  super(context);
  initMyView();
 }

 public AnimationView(Context context, AttributeSet attrs) {
  super(context, attrs);
  initMyView();
 }

 public AnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  initMyView();
 }
 
 public void initMyView(){
  paint = new Paint();
  paint.setColor(Color.BLUE);
  paint.setStrokeWidth(1);
  paint.setStyle(Paint.Style.STROKE);
    
  bm = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
  bm_offsetX = bm.getWidth()/2;
  bm_offsetY = bm.getHeight()/2;
  
  animPath = new Path();
  
  pos = new float[2];
  tan = new float[2];
  
  matrix = new Matrix();
  
  touchPath = new Path();
  
  step = 1;  //default
  stepAngle = 1; //default
 }

 @Override
 protected void onDraw(Canvas canvas) {
  if(animPath.isEmpty()){
   return;
  }
  
  canvas.drawPath(animPath, paint);
  
  matrix.reset();
  
  if((targetAngle-curAngle)>stepAngle){
   curAngle += stepAngle;
   matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
   matrix.postTranslate(curX, curY);
   canvas.drawBitmap(bm, matrix, null);
   
   invalidate();
  }else if((curAngle-targetAngle)>stepAngle){
   curAngle -= stepAngle;
   matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
   matrix.postTranslate(curX, curY);
   canvas.drawBitmap(bm, matrix, null);
   
   invalidate();
  }else{
   curAngle=targetAngle;
   if(distance < pathLength){
    pathMeasure.getPosTan(distance, pos, tan);

    targetAngle = (float)(Math.atan2(tan[1], tan[0])*180.0/Math.PI);
    matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
    
    curX = pos[0]-bm_offsetX;
    curY = pos[1]-bm_offsetY;
    matrix.postTranslate(curX, curY);
    
    canvas.drawBitmap(bm, matrix, null);
    
    distance += step;
    
    invalidate();
   }else{
    matrix.postRotate(curAngle, bm_offsetX, bm_offsetY);
    matrix.postTranslate(curX, curY);
    canvas.drawBitmap(bm, matrix, null);
   }
  }

 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  
  int action = event.getAction();
  
  switch(action){
  case MotionEvent.ACTION_DOWN:
   touchPath.reset();
   touchPath.moveTo(event.getX(), event.getY());
   break;
  case MotionEvent.ACTION_MOVE:
   touchPath.lineTo(event.getX(), event.getY());
   break;
  case MotionEvent.ACTION_UP:
   touchPath.lineTo(event.getX(), event.getY());
   animPath = new Path(touchPath);
   
   pathMeasure = new PathMeasure(animPath, false);
   pathLength = pathMeasure.getLength();
   
   //step = 1;
   distance = 0;
   curX = 0;
   curY = 0;
   
   //stepAngle = 1; 
   curAngle = 0;
   targetAngle = 0;
   
   invalidate();
   
   break;
    
  }
  
  return true;
 }
 
 public void setSpeed(int sp){
  step = sp;
  stepAngle = sp;
 }

}

/res/layout/activity_main.xml
<LinearLayout 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:orientation="vertical"
    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="com.example.androidanimationalongpath.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <SeekBar
        android:id="@+id/speedbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="10"
        android:progress="1"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >
        
        <com.example.androidanimationalongpath.AnimationView
            android:id="@+id/animationview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="10dp"
            android:background="@android:color/darker_gray" />

    </LinearLayout>

</LinearLayout>

MainActivity.java
package com.example.androidanimationalongpath;

import android.support.v7.app.ActionBarActivity;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.os.Bundle;

public class MainActivity extends ActionBarActivity {
 
 AnimationView animationView;
 SeekBar speedBar;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  animationView = (AnimationView)findViewById(R.id.animationview);
  speedBar = (SeekBar)findViewById(R.id.speedbar);
  speedBar.setOnSeekBarChangeListener(speedBarOnSeekBarChangeListener);
  animationView.setSpeed(speedBar.getProgress()); //set default speed
 }
 
 OnSeekBarChangeListener speedBarOnSeekBarChangeListener = 
  new OnSeekBarChangeListener(){

   @Override
   public void onProgressChanged(SeekBar seekBar, int progress,
     boolean fromUser) {
    //offset from SeekBar 0~19 to step 1~10
    animationView.setSpeed(progress);
   }

   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {}

   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {}};
   

}


download filesDownload the files.

More example of Drawing Path on canvas of custom View.