------

[ AD ] Port Monitor ( Try to use a Best WebSite Monitoring Tool )

------

------------------------------------------------------------------

두 날짜의 차이를 일수로 구하기
------------------------------------------------------------------
각각의 날짜를 Date형으로 만들어서 getTime()하면
long으로 값이 나오거든요(1970년 1월 1일 이후-맞던가?- 1/1000 초 단위로..)
그러면 이값의 차를 구해서요. (1000*60*60*24)로 나누어 보면 되겠죠.
------------------------------------------------------------------

두 날짜의 차이를 일수로 구하기2
------------------------------------------------------------------

import java.io.*;
import java.util.*;

Date today = new Date ( );
Calendar cal = Calendar.getInstance ( );
cal.setTime ( today );// 오늘로 설정. 

Calendar cal2 = Calendar.getInstance ( );
cal2.set ( 2000, 3, 12 ); // 기준일로 설정. month의 경우 해당월수-1을 해줍니다.

int count = 0;
while ( !cal2.after ( cal ) )
{
count++;
cal2.add ( Calendar.DATE, 1 ); // 다음날로 바뀜

System.out.println ( cal2.get ( Calendar.YEAR ) + "년 " + ( cal2.get ( Calendar.MONTH ) + 1 ) + "월 " + cal2.get ( Calendar.DATE ) + "일" );
}

System.out.println ( "기준일로부터 " + count + "일이 지났습니다." );



------------------------------------------------------------------

두 날짜의 차이를 일수로 구하기3
------------------------------------------------------------------

import java.io.*;
import java.util.*;

public class DateDiff
{
public static int GetDifferenceOfDate ( int nYear1, int nMonth1, int nDate1, int nYear2, int nMonth2, int nDate2 )
{
Calendar cal = Calendar.getInstance ( );
int nTotalDate1 = 0, nTotalDate2 = 0, nDiffOfYear = 0, nDiffOfDay = 0;

if ( nYear1 > nYear2 )
{
for ( int i = nYear2; i < nYear1; i++ ) 
{
cal.set ( i, 12, 0 );
nDiffOfYear += cal.get ( Calendar.DAY_OF_YEAR );
}
nTotalDate1 += nDiffOfYear;
}
else if ( nYear1 < nYear2 )
{
for ( int i = nYear1; i < nYear2; i++ )
{
cal.set ( i, 12, 0 );
nDiffOfYear += cal.get ( Calendar.DAY_OF_YEAR );
}
nTotalDate2 += nDiffOfYear;
}

cal.set ( nYear1, nMonth1-1, nDate1 );
nDiffOfDay = cal.get ( Calendar.DAY_OF_YEAR );
nTotalDate1 += nDiffOfDay;

cal.set ( nYear2, nMonth2-1, nDate2 );
nDiffOfDay = cal.get ( Calendar.DAY_OF_YEAR );
nTotalDate2 += nDiffOfDay;

return nTotalDate1-nTotalDate2;
} 

public static void main ( String args[] )
{
System.out.println ( "" + GetDifferenceOfDate (2000, 6, 15, 1999, 8, 23 ) );
}
}


ing the following example we can display the thumbnails of images stored in the sd card. Here im displaying the thumbnails in the Grid view. You should to add the folowing permission in the
manifest xml file.

< uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

Sd card should be available with the emulator or real device.
ImageThumbnailsActivity is the main Activity. AndroidManifest.xml

< manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="image.Thumbnails" android:versionCode="1" android:versionName="1.0.0">
      < uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
      < application android:icon="@drawable/icon" android:label="@string/app_name">
            < activity android:name=".ImageThumbnailsActivity"
                  android:label="@string/app_name">
                  < intent-filter>
                        < action android:name="android.intent.action.MAIN" />
                        < category android:name="android.intent.category.LAUNCHER"/>
                  
            
            < activity android:name=".ViewImage">
                  < intent-filter>
                        < action android:name="android.intent.action.VIEW" />
                        < category android:name="android.intent.category.DEFAULT" />
                  
            
      




package image.Thumbnails;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.AdapterView.OnItemClickListener;

public class ImageThumbnailsActivity extends Activity {
      /** Called when the activity is first created. */
      private Cursor imagecursor, actualimagecursor;
      private int image_column_index, actual_image_column_index;
      GridView imagegrid;
      private int count;
      @Override
      public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            init_phone_image_grid();
      }
      private void init_phone_image_grid() {
            String[] img = { MediaStore.Images.Thumbnails._ID };
            imagecursor = managedQuery(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, img, null,
null, MediaStore.Images.Thumbnails.IMAGE_ID + "");
            image_column_index = imagecursor
.getColumnIndexOrThrow(MediaStore.Images.Thumbnails._ID);
            count = imagecursor.getCount(); 
            imagegrid = (GridView) findViewById(R.id.PhoneImageGrid);
            imagegrid.setAdapter(new ImageAdapter(getApplicationContext()));
            imagegrid.setOnItemClickListener(new OnItemClickListener() {
                  public void onItemClick(AdapterView parent, View v,
int position, long id) {
                        System.gc();
                        String[] proj = { MediaStore.Images.Media.DATA };
                        actualimagecursor = managedQuery(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, proj,
null, null, null);
                        actual_image_column_index = actualimagecursor
.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                        actualimagecursor.moveToPosition(position);
                        String i = actualimagecursor.getString(actual_image_column_index);
                        System.gc();
                        Intent intent = new Intent(getApplicationContext(), ViewImage.class);
                        intent.putExtra("filename", i);
                        startActivity(intent);
                  }
            });
      }



public class ImageAdapter extends BaseAdapter {
            private             Context mContext;
            public ImageAdapter(Context c) {
                  mContext = c;
            }
            public int getCount() {
                  return count;
            }
            public Object getItem(int position) {
                  return position;
            }
            public long getItemId(int position) {
                  return position;
            }
            public View getView(int position,View convertView,ViewGroup parent) {
                  System.gc();
                  ImageView i = new ImageView(mContext.getApplicationContext());
                  if (convertView == null) {
                        imagecursor.moveToPosition(position);
                        int id = imagecursor.getInt(image_column_index);
                        i.setImageURI(Uri.withAppendedPath(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, ""
+ id));
                        i.setScaleType(ImageView.ScaleType.CENTER_CROP);
                        i.setLayoutParams(new GridView.LayoutParams(92, 92));
                  }
                  else {
                        i = (ImageView) convertView;
                  }
                  return i;
            }
      }
}



// By selecting the thumbnails user can view the actual image.
package image.Thumbnails;

import android.os.Bundle;
import android.widget.ImageView;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

public class ViewImage extends Activity {
      private String filename;
      @Override
      public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            System.gc();
            Intent i = getIntent();
            Bundle extras = i.getExtras();
            BitmapFactory.Options bfo = new BitmapFactory.Options();
            bfo.inSampleSize = 2;
            filename = extras.getString("filename");
            ImageView iv = new ImageView(getApplicationContext());
            Bitmap bm = BitmapFactory.decodeFile(filename, bfo);
            iv.setImageBitmap(bm);
            setContentView(iv);
      } 
}




main.xml
< ?xml version="1.0" encoding="utf-8"?>
< LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
< GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/PhoneImageGrid" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:numColumns="auto_fit"
android:verticalSpacing="10dp" android:horizontalSpacing="10dp"
android:columnWidth="90dp" android:stretchMode="columnWidth"
android:gravity="center" />
< /LinearLayout> 

'0.일반개발' 카테고리의 다른 글

안드로이드 타원형 박스  (0) 2010.09.14
날짜 안드로이드 자바 일수 계산  (0) 2010.09.14
Thumb Viewer Image  (0) 2010.09.14
OpenGL ES 1.0 on Android – Triangle Example  (2) 2010.09.14
팝업 윈도우  (1) 2010.09.14
package android.com.app;   
   
import java.util.ArrayList;   
   
import android.app.Activity;   
import android.content.ContentResolver;   
import android.content.Context;   
import android.content.Intent;   
import android.database.Cursor;   
import android.net.Uri;   
import android.os.Bundle;   
import android.provider.MediaStore;   
import android.provider.MediaStore.Images;   
import android.util.Log;   
import android.view.Menu;   
import android.view.MenuItem;   
   
import android.view.View;   
import android.view.ViewGroup;   
import android.widget.AdapterView;   
import android.widget.BaseAdapter;   
import android.widget.GridView;   
import android.widget.ImageView;   
import android.widget.TextView;   
import android.widget.AdapterView.OnItemClickListener;   
import android.widget.AdapterView.OnItemLongClickListener;   
   
import com.google.android.maps.GeoPoint;   
   
public class thumbviewer extends Activity {   
    /** Called when the activity is first created. */   
           
        private Context mContext;   
           
        @Override   
            public void onCreate(Bundle savedInstanceState) {   
                super.onCreate(savedInstanceState);   
                setContentView(R.layout.apicthumbsview);   
   
                GridView g = (GridView) findViewById(R.id.grid1);   
                final ImageAdapter ia = new ImageAdapter(this);   
                g.setAdapter(ia);   
                g.setOnItemClickListener(new OnItemClickListener() {   
                   
                public void onItemClick(AdapterView parent, View v, int position, long id) {   
                                ia.callImageViewer(position);   
                    }   
                });   
                   
                g.setOnItemLongClickListener(new OnItemLongClickListener() {   
                           
                        public boolean onItemLongClick(AdapterView parent, View v, 
int position, long id) { return ia.deleteSelected(position); } }); } protected void onPause() { super.onPause(); finish(); }

 public class ImageAdapter extends BaseAdapter {   
           
        private ArrayList imgList;   
            private ArrayList geoList;   
            private ArrayList titleList;   
            private ArrayList thumbsList;   
               
           
        public ImageAdapter(Context c) {   
            mContext = c;   
            imgList=new ArrayList();   
            geoList=new ArrayList();   
            titleList=new ArrayList();   
            thumbsList=new ArrayList();   
            getImageInfo(imgList,geoList,titleList,thumbsList);   
        }   
           
        public boolean deleteSelected(int sIndex){   
                   
                getContentResolver().delete(Uri.withAppendedPath(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, ""+imgList.get(sIndex)), null, null); getContentResolver().delete(Uri.withAppendedPath(M
ediaStore.Images.Media.EXTERNAL_CONTENT_URI, ""+imgList.get(sIndex)), null, null); imgList.remove(sIndex); titleList.remove(sIndex); geoList.remove(sIndex); return true; } public final void callImageViewer(int selectedIndex){ TextView t = (TextView) findViewById(R.id.txt1); Intent i = new Intent(); i.setClass(mContext, imageviewer.class); t.setText("select"+img
List.get(selectedIndex)); i.putExtra("ImageData", imgList.get(selectedIndex)); //ID of the image i.putExtra("GeoPoint",
geoList.get(selectedIndex).getLatitudeE6()+","+geoList.get(selectedIndex).getLongitudeE6()); i.putExtra("Title", titleList.get(selectedIndex)); startActivityForResult(i,1); } public int getCount() { return imgList.size(); } public Object getItem(int position) { return position; } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { ImageView imageView; if (convertView == null) { imageView = new ImageView(mContext); imageView.setLayoutParams(new GridView.LayoutParams(85,85)); imageView.setAdjustViewBounds(false); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setPadding(8, 8, 8, 8); } else { imageView = (ImageView) convertView; } TextView t = (TextView) findViewById(R.id.txt1); System.gc(); try{ Log.i("BMP","size"+imgList.size()); //Bitmap bmp = BitmapFactory.decodeFile(thumbsList.get(position)); imageView.setImageURI(Uri.withAppendedPath(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, ""+imgList.get(position))); //imageView.setImageURI(Uri.parse(thumbsList.get(position))); //Log.i("THUMBNAILS DATA" ,Uri.withAppendedPath(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, titleList.get(position)).toString()); System.gc(); //imageView.setImageBitmap(bmp); } catch(Exception e){ t.setText(e.getMessage()); } return imageView; }
/* Get all images from content provider in an array list*/ public void getImageInfo(ArrayList ThumbsIDList,ArrayList LocationList,ArrayList ThumbsDataList,ArrayList ThumbsList){ TextView t = (TextView) findViewById(R.id.txt1); String[] imageAttribs = new String[] { //Images.ImageColumns.TITLE, Images.Thumbnails._ID, //Images.ImageColumns.DISPLAY_NAME, //Images.ImageColumns.DATA, //Images.ImageColumns.LATITUDE, //Images.ImageColumns.LONGITUDE, Images.Thumbnails.DATA, Images.Thumbnails.IMAGE_ID, }; Cursor imageCursor= managedQuery( MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, imageAttribs, null, null, // WHERE clause. null); if (imageCursor !=null && imageCursor.moveToFirst()) { String title; String thumbsID; String thumbsImageID; String thumbsData; String data; //int titleColumn = imageCursor.getColumnIndex(Images.ImageColumns.DATA); int thumbsIDcol = imageCursor.getColumnIndex(Images.Thumbnails._ID); //int LatCol = imageCursor.getColumnIndex(Images.ImageColumns.LATITUDE); //int LongCol = imageCursor.getColumnIndex(Images.ImageColumns.LONGITUDE); int thumbsDataCol = imageCursor.getColumnIndex(Images.Thumbnails.DATA); int thumbsImageIDCol = imageCursor.getColumnIndex(Images.Thumbnails.IMAGE_ID); int num=0; do { // Get the field values t.setText("d"+num); //title = imageCursor.getString(titleColumn); thumbsID = imageCursor.getString(thumbsIDcol); thumbsData = imageCursor.getString(thumbsDataCol); thumbsImageID = imageCursor.getString(thumbsImageIDCol); Log.i("BMP","size "+thumbsID+" "+thumbsData+" "+thumbsImageID); num++; if(thumbsImageID!= null) { ThumbsIDList.add(imageCursor.getString(thumbsIDcol)); ThumbsDataList.add(imageCursor.getString(thumbsDataCol)); } //TitleList.add(title); //t.setText(imageCursor.getDouble(LatCol)+","+imageCursor.getDouble(LongCol)); //Comment this for emu //LocationList.add(new GeoPoint((int)34.027412*1000000,(int)-118.289738*1000000)); //Comment this for device /*LocationList.add(new GeoPoint( (int)(imageCursor.getDouble(LatCol)*1000000), (int)(imageCursor.getDouble(LongCol)*1000000) ) ); */ //LocationList.add(new GeoPoint( // (int)(imageCursor.getDouble(LatCol)*1000000), // (int)(imageCursor.getDouble(LongCol)*1000000) // ) // ); } while (imageCursor.moveToNext()); } return; } }//class imageadapter protected void onDestroy() { super.onDestroy(); finish(); } }//class apicsthumbview>>>>>>> .r67

'0.일반개발' 카테고리의 다른 글

날짜 안드로이드 자바 일수 계산  (0) 2010.09.14
How to Display Thumbnails of Images  (0) 2010.09.14
OpenGL ES 1.0 on Android – Triangle Example  (2) 2010.09.14
팝업 윈도우  (1) 2010.09.14
Android 2.0 변경사항  (0) 2010.09.14
OpenGL ES 1.0 on 
Android – Triangle Example 
package com.ruibm;
 
import android.app.Activity;
import android.graphics.PointF;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
 
public class MainActivity extends Activity {
 
  private PointF touchStart;
  private GLSurfaceView glSurfaceView;  
  private TriangleRenderer renderer;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    touchStart = new PointF();
    glSurfaceView = new GLSurfaceView(this);
    renderer = new TriangleRenderer();
    glSurfaceView.setRenderer(renderer);
    setContentView(glSurfaceView);
  }
 
  @Override
  protected void onResume() {
    super.onResume();
    glSurfaceView.onResume();
  }
 
  @Override
  protected void onPause() {
    super.onPause();
    glSurfaceView.onPause();
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_MOVE:
        renderer.move(event.getX() - touchStart.x, touchStart.y - event.getY());
        touchStart.set(event.getX(), event.getY());
        glSurfaceView.requestRender();
        return true;
 
      case MotionEvent.ACTION_DOWN:
        touchStart.set(event.getX(), event.getY());
        return true;
 
      default:
        return super.onTouchEvent(event);
    }
  }
 
  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    switch (keyCode) {
      case KeyEvent.KEYCODE_DPAD_UP:
        renderer.move(0, 1);
        glSurfaceView.requestRender();
        return true;
 
      case KeyEvent.KEYCODE_DPAD_DOWN:
        renderer.move(0, -1);
        glSurfaceView.requestRender();
        return true;
 
      case KeyEvent.KEYCODE_DPAD_LEFT:
        renderer.move(-1, 0);
        glSurfaceView.requestRender();
        return true;
 
      case KeyEvent.KEYCODE_DPAD_RIGHT:
        renderer.move(1, 0);
        glSurfaceView.requestRender();
        return true;
 
      default:
        return super.onKeyDown(keyCode, event);      
    }
  }
}

package com.ruibm;
 
import java.nio.ShortBuffer;
 
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
 
import android.graphics.PointF;
import android.opengl.GLSurfaceView.Renderer;
import android.util.Log;
 
public class TriangleRenderer implements Renderer {
 
  private PointF surfaceSize;
  private PointF offset;
  private ShortBuffer triangleBuffer;
 
  public TriangleRenderer() {
    surfaceSize = new PointF();
    offset = new PointF();
  }
 
  public void onSurfaceCreated(GL10 gl, EGLConfig config) {
  }
 
  public void onSurfaceChanged(GL10 gl, int width, int height) {
    surfaceSize.set(width, height);
 
    // Create our triangle.
    final int div = 1;
    short[] triangles = { 
        0, 0, 0,
        0, (short) (surfaceSize.y / div), 0,
        (short) (surfaceSize.x / div), (short) (surfaceSize.y / div), 0,
    };    
    triangleBuffer = ShortBuffer.wrap(triangles);
 
    // Disable a few things we are not going to use.
    gl.glDisable(GL10.GL_LIGHTING);
    gl.glDisable(GL10.GL_CULL_FACE);
    gl.glDisable(GL10.GL_DEPTH_BUFFER_BIT);
    gl.glDisable(GL10.GL_DEPTH_TEST);
    gl.glClearColor(.5f, .5f, .8f, 1.f);
    gl.glShadeModel(GL10.GL_SMOOTH);
 
    float ratio = surfaceSize.x / surfaceSize.y;
    gl.glViewport(0, 0, (int) surfaceSize.x, (int) surfaceSize.y);
 
    // Set our field of view.
    gl.glMatrixMode(GL10.GL_PROJECTION);
    gl.glLoadIdentity();
    gl.glFrustumf(
        -surfaceSize.x / 2, surfaceSize.x / 2, 
        -surfaceSize.y / 2, surfaceSize.y / 2,
        1, 3);
 
    // Position the camera at (0, 0, -2) looking down the -z axis.
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    // Points rendered to z=0 will be exactly at the frustum's 
    // (farZ - nearZ) / 2 so the actual dimension of the triangle should be
    // half
    gl.glTranslatef(0, 0, -2);
  }
 
  public void onDrawFrame(GL10 gl) {    
    gl.glPushMatrix();
 
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    gl.glTranslatef(offset.x, offset.y, 0);
 
    gl.glColor4f(1.0f, 0.3f, 0.0f, .5f);    
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glVertexPointer(3, GL10.GL_SHORT, 0, triangleBuffer);
    gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 3);    
    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
 
    gl.glPopMatrix();
  }
 
  public void move(float xDelta, float yDelta) {
    offset.x += xDelta;
    offset.y += yDelta;
    Log.d("TriangleRenderer", "offset=[" + offset.x + " ," + offset.y + "]");
  }
}
 

 

Android 3D game tutorial – Part I

 

The first part of this series will give you a short introduction to the OpenGL terminology and the first step in your 3D programming.

The series itself will be about a 3D game called Vortex.
The tutorial will focus on 3D programming, stuff like menu or life cycle may be part of the code but will not be introduced.

Lets start with the terminology of OpenGL.

'0.일반개발' 카테고리의 다른 글

How to Display Thumbnails of Images  (0) 2010.09.14
Thumb Viewer Image  (0) 2010.09.14
팝업 윈도우  (1) 2010.09.14
Android 2.0 변경사항  (0) 2010.09.14
안드로이드 제스쳐 Android Gesture  (0) 2010.09.14

http://stackoverflow.com/questions/1967863/is-there-a-simple-example-of-the-popupwindow-class-using-android-v2-0

 

 

I created a working example based on this Google Groups post.

To create a simple working PopupWindow, you'll need to do the following:

  1. Create a layout XML which describes the View that will be rendered within the PopupWindow.
  2. Invoke the PopupWindow by inflating the layout XML, and assign the appropriate "parent view" to the pop-up.

popup_example.xml:

<?xml version="1.0" encoding="utf-8"?> 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:padding="10dip" 
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    > 
 
    <TextView 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" 
        android:text="Test Pop-Up" 
    /> 
 
</LinearLayout> 

Java code:

    LayoutInflater inflater = (LayoutInflater) 
       this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 
    PopupWindow pw = new PopupWindow( 
       inflater.inflate(R.layout.popup_example, null, false),  
       100,  
       100,  
       true); 
    // The code below assumes that the root container has an id called 'main' 
    pw.showAtLocation(this.findViewById(R.id.main), Gravity.CENTER, 0, 0);  
 
 
 
http://rsequence.com/android_blog/node/160
private DismissPopup mDismissPopup = new DismissPopup();

if (mShowToast) {
    LayoutInflater inflater;
    inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mPopupView = inflater.inflate(R.layout.month_bubble, null);
    mPopup = new PopupWindow(activity);
    mPopup.setContentView(mPopupView);
    Resources.Theme dialogTheme = getResources().newTheme();
    dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
    TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
        android.R.attr.windowBackground });
    mPopup.setBackgroundDrawable(ta.getDrawable(0));
    ta.recycle();
}
        
        
if (mShowToast) {
    mPopup.dismiss();
    mPopup.setWidth(width - 20);
    mPopup.setHeight(POPUP_HEIGHT);
}
        
        
mPopup.setHeight(popupHeight);

if (mPreviousPopupHeight != popupHeight) {
    mPreviousPopupHeight = popupHeight;
    mPopup.dismiss();
}
mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, 0, 0);
postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);


class DismissPopup implements Runnable {
    public void run() {
        mPopup.dismiss();
    }
}

// This is called when the activity is paused so that the popup can
// be dismissed.
void dismissPopup() {
    if (!mShowToast) {
        return;
    }

    // Protect against null-pointer exceptions
    if (mPopup != null) {
        mPopup.dismiss();
    }

    Handler handler = getHandler();
    if (handler != null) {
        handler.removeCallbacks(mDismissPopup);
    }
}

API changes summary

 
안드로이드 제스쳐





reating a gestures library
 
Loading the gestures library
 
Recognizing gestures
 
Gestures overlay
 
==================== 
 
Like it or not touch screens are becoming part of both developers and users life, i mean dont think i would buy one of Those uncool phones without touchscreen unless i really have to, would you? but the important point is that what is the Touchscreen use if applications doesn't support touchscreen interaction, in other words who is really gonna pay for a ,say, Picture management application if it does not support some gestures for switching between pictures or zooming? [ Gesture Detecti
on in Android ].
In Android there are three levels of touch screen event handling mechanism which can be used by developers. the most low level technique is to receive all touch events and take care of all things, you can attach an 'OnTouchListener' to a view and get notified whenever there is a touch event or you can override onTouchEvent() or dispatchTouchEvent() method of your activity or view, in all these cases you would be dealing with an instance of MotionEvent every single time and you would have to detect what user is doing all on your own which will suit requirments for developing games and stuff like that. But it is just too much hassle if you only need a few simple gestures for your application.

Next approach is to use GestureDetector class along with OnGestureListener and/or OnDoubleTapListener, in this technique whenever there is a new MotionEvent you have to pass it to Gesture Detector's onTouchEvent() method, it then will analyse this event and previous events and tell you what is happening on the screen by calling some of the callback methods.

here is a simple activity which uses GestureDetector :

 public class SimpleActivity extends Activity
 implements OnDoubleTapListener,
  android.view.GestureDetector.OnGestureListener { 

 private GestureDetector detector;     
 /** Called when the activity is first created. */   
 @Override   
 public void onCreate(Bundle savedInstanceState)
 {    
  super.onCreate(savedInstanceState);       
  setContentView(R.layout.simple);                
  detector = new GestureDetector(this,this);   
  
 }    
 @Override 
 public boolean onTouchEvent(MotionEvent me)
 {   
  this.detector.onTouchEvent(me); 
  return super.onTouchEvent(me); 
  
 }
 //@Override
 public boolean onDown(MotionEvent e)
 {   
  Log.d("---onDown----",e.toString());    
  return false;
  
 }
 //@Override
 public boolean onFling(MotionEvent e1,
   MotionEvent e2, float velocityX,  
   float velocityY)
 { 
  Log.d("---onFling---",e1.toString()+e2.toString());  
  return false;
  
 }
 //@Override
 public void onLongPress(MotionEvent e)
 { 
  Log.d("---onLongPress---",e.toString()); 
  
 }
 //@Override
 public boolean onScroll(MotionEvent e1,
   MotionEvent e2, float distanceX,   float distanceY)
 { 
  Log.d("---onScroll---",e1.toString()+e2.toString()); 
  return false;
  
 }
 //@Override
 public void onShowPress(MotionEvent e)
 { 
  Log.d("---onShowPress---",e.toString());
  
 }
 //@Override
 public boolean onSingleTapUp(MotionEvent e)
 { 
  Log.d("---onSingleTapUp---",e.toString()); 
  return false;
  
 }
 //@Override
 public boolean onDoubleTap(MotionEvent e)
 { 
  Log.d("---onDoubleTap---",e.toString()); 
  return false;
  
 }
 //@Override
 public boolean onDoubleTapEvent(MotionEvent e)
 { 
  Log.d("---onDoubleTapEvent---",e.toString());  
  return false;
  
 }
 //@Override
 public boolean onSingleTapConfirmed(MotionEvent e)
 { 
  Log.d("---onSingleTapConfirmed---",e.toString()); 
  return false;
  
 }

 
}

 
if we want to understand all gesture types and how they work, first of all we need to know three basic MotionEvents which can combine with each other and create some gestures, these three Events are Action_Down , Action_Move and Action_Up , each time you touch the screen an Action_Down occurs and when you start moving it will create Action_Move event and finally when you take your finger off the screen an Action_Up Event will be created.
onDown() is called simply when there is an Action_Down event.
onShowPress() is called after onDown() and before any other callback method, I found out it sometimes might not get called for example when you tap on the screen so fast, but it's actually what this method all about, to make a distinction between a possible unintentional touch and an intentional one.
onSingleTapUp() is called when there is a Tap Gesture. Tap Gesture happens when an Action_Down event followed by an Action_Up event(like a Single-Click).
onDoubleTap() is called when there is two consecutive Tap gesture(like a Double-Click).
onSingleTapConfirmed() is so similar to onSingleTapUp() but it is only get called when the detected tap gesture is definitely a single Tap and not part of a double tap gesture.
Here is the sequence in which these callback methods are called when we tap on the screen:

onDown() – onShowPress() - onSingleTapUp() – onSingleTapConfirmed()


and here is when we do a double tap:


onDown() – onShowPress() - onSingleTapUp() – onDoubleTap() – onDoubleTapEvent()
onDown() – onShowPress() – onDoubleTapEvent()


onFling() is called when a Fling Gesture is detected. fling Gesture occurs when there is an Action_Down then some Action_Move events and finally an Action_Up, but they must take place with a specified movement and velocity pattern to be considered as a fling gesture. for example if you put your finger on the screen and start moving it slowly and then remove your finger gently it won’t be count as a fling gesture.

onScroll() is usually called when there is a Action_Move event so if you put your finger on the screen and move it for a few seconds there will be a method call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... - onScroll()


or If the movement was a Fling Gesture, then there would be a call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... – onFling()

If there is an Action_Move event between first tap and second tap of a doubleTap gesture it will be handled by calling onDoubleTapEvent() instead of onScroll() method. onDoubleTapEvent() receives all Action_Up events of a doubleTap gesture as well.

Remember that if we touch the screen and don’t remove our finger for a specified amount of time onLongPress() method is called and in most cases there will be no further event callback regardless of whatever we do after that, moving or removing our finger. we can easily change this behavior of detector by calling setIsLongpressEnabled() method of GestureDetector class.

although GestureDetector makes our life much easier, it still could be a real pain in the ass if we would need to handle some complicated gestures, imagine you need an application which should do task1 when there is a circle gesture, task2 for rectangle gesture and task3 for triangle gesture. obviously it would not be so pleasant to deal with such a situation with those mechanism we have seen so far, it's actually when Gesture API and Gesture Builder Application come into play.

Gesture Builder Application comes with Android emulator as a pre-installed app, It helps us to simply make new gestures and save them on the device, then we can retrieve and detect those gestures later using Gesture API.
 I'm not gonna talk about Gesture Builder app here, 제스처 빌더를 여기서 말하려 하는거 아니다.
since there is a pretty good Article about it on Android Developers blog.
 
// 제스처 오버레이 뷰 클래스
the only thing I'd like to mention here is GestureOverlayView class,
 It is actually just a transparent FrameLayout which can detect gestures but the thing is it can be used in two completely different ways, you can either put other views inside it and use it as a parent view or put it is the last child view of a FrameLayout (or any other way which causes it to be placed on top of another view).
 
//첫번째 시나리오  : 모든 차일드 뷰가 터치 이벤트를 받는다..
In the first Scenario all child views will receive Touch events,
 therefore we will be able to press buttons or interact with other widgets as well as doing some gestures, on the other hand if GestureOverlayView has been placed on top, it will swallow all Touch events and no underlay view will be notified for any touch Event.

Although Gesture API brings many useful features for us,
I personally prefer to use GestureDetector for some simple,
basic gestures and honestly I feel like something is missing here,
I mean , apart from games, I would say more than 70% of all gestures
that might be needed in our applications are just a simple sliding in different directions or a double tap.
and that's why I have decided to come up with something easy
which can enable us to handle those 70% as simple as possible. how simple? you might be asking...

here is how our previous activity will look like if we use SimpleGestureFilter class :

 public class SimpleActivity extends Activity implements SimpleGestureListener{

private SimpleGestureFilter detector;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        detector = new SimpleGestureFilter(this,this);
    }
  
  @Override
public boolean dispatchTouchEvent(MotionEvent me){
   this.detector.onTouchEvent(me);
  return super.dispatchTouchEvent(me);
}

  @Override
public void onSwipe(int direction) {
  String str = "";
 
  switch (direction) {
 
  case SimpleGestureFilter.SWIPE_RIGHT : str = "Swipe Right";
                                           break;
  case SimpleGestureFilter.SWIPE_LEFT :  str = "Swipe Left";
                                                 break;
  case SimpleGestureFilter.SWIPE_DOWN :  str = "Swipe Down";
                                                 break;
  case SimpleGestureFilter.SWIPE_UP :    str = "Swipe Up";
                                                 break;
                                          
  }
   Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}

@Override
public void onDoubleTap() {
    Toast.makeText(this, "Double Tap", Toast.LENGTH_SHORT).show();
}

}

 

and here is SimpleGestureFilter source code :

 public class SimpleGestureFilter extends SimpleOnGestureListener{
   
public final static int SWIPE_UP    = 1;
public final static int SWIPE_DOWN  = 2;
public final static int SWIPE_LEFT  = 3;
public final static int SWIPE_RIGHT = 4;

public final static int MODE_TRANSPARENT = 0;
public final static int MODE_SOLID       = 1;
public final static int MODE_DYNAMIC     = 2;

private final static int ACTION_FAKE = -13; //just an unlikely number
private int swipe_Min_Distance = 100;
private int swipe_Max_Distance = 350;
private int swipe_Min_Velocity = 100;

private int mode      = MODE_DYNAMIC;
private boolean running = true;
private boolean tapIndicator = false;

private Activity context;
private GestureDetector detector;
private SimpleGestureListener listener;


public SimpleGestureFilter(Activity context,SimpleGestureListener sgl) {

  this.context = context;
  this.detector = new GestureDetector(context, this);
  this.listener = sgl;
}

public void onTouchEvent(MotionEvent event){
 
   if(!this.running)
  return; 
 
   boolean result = this.detector.onTouchEvent(event);
 
   if(this.mode == MODE_SOLID)
    event.setAction(MotionEvent.ACTION_CANCEL);
   else if (this.mode == MODE_DYNAMIC) {
 
     if(event.getAction() == ACTION_FAKE)
       event.setAction(MotionEvent.ACTION_UP);
     else if (result)
       event.setAction(MotionEvent.ACTION_CANCEL);
     else if(this.tapIndicator){
      event.setAction(MotionEvent.ACTION_DOWN);
      this.tapIndicator = false;
     }
 
   }
   //else just do nothing, it's Transparent
}

public void setMode(int m){
  this.mode = m;
}

public int getMode(){
  return this.mode;
}

public void setEnabled(boolean status){
  this.running = status;
}

public void setSwipeMaxDistance(int distance){
  this.swipe_Max_Distance = distance;
}

public void setSwipeMinDistance(int distance){
  this.swipe_Min_Distance = distance;
}

public void setSwipeMinVelocity(int distance){
  this.swipe_Min_Velocity = distance;
}

public int getSwipeMaxDistance(){
  return this.swipe_Max_Distance;
}

public int getSwipeMinDistance(){
  return this.swipe_Min_Distance;
}

public int getSwipeMinVelocity(){
  return this.swipe_Min_Velocity;
}


@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
   float velocityY) {

  final float xDistance = Math.abs(e1.getX() - e2.getX());
  final float yDistance = Math.abs(e1.getY() - e2.getY());

  if(xDistance > this.swipe_Max_Distance || yDistance > this.swipe_Max_Distance)
   return false;

  velocityX = Math.abs(velocityX);
  velocityY = Math.abs(velocityY);
        boolean result = false;

if(velocityX > this.swipe_Min_Velocity && xDistance > this.swipe_Min_Distance){
   if(e1.getX() > e2.getX()) // right to left
    this.listener.onSwipe(SWIPE_LEFT);
   else
    this.listener.onSwipe(SWIPE_RIGHT);
  
   result = true;
  }
  else if(velocityY > this.swipe_Min_Velocity && yDistance > this.swipe_Min_Distance){
   if(e1.getY() > e2.getY()) // bottom to up
    this.listener.onSwipe(SWIPE_UP);
   else
    this.listener.onSwipe(SWIPE_DOWN);
  
   result = true;
  }

   return result;
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
  this.tapIndicator = true;
  return false;
}

@Override
public boolean onDoubleTap(MotionEvent arg0) {
  this.listener.onDoubleTap();;
  return true;
}

@Override
public boolean onDoubleTapEvent(MotionEvent arg0) {
  return true;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent arg0) {
 
  if(this.mode == MODE_DYNAMIC){        // we owe an ACTION_UP, so we fake an      
     arg0.setAction(ACTION_FAKE);      //action which will be converted to an ACTION_UP later.                                   
     this.context.dispatchTouchEvent(arg0); 
  }  
    
  return false;
}


    static interface SimpleGestureListener{
     void onSwipe(int direction);
     void onDoubleTap();
}

}

 

as you can see clients of these class can determine the minimum and maximum distance and also minimum velocity which is required for a movement on screen to be considered as a Swipe Gesture,

 

I also thought it would be great if our filter can behave differently like what GestureOverlayView

can do and even more than that!


this Filter can run in three different mode:

Transparent, Solid and Dynamic.

 

in Transparent mode

it will work just like when we have a GestureOverlayView as parent: all views will receive Touch events;

Solid mode works

like when we put a GestureOverlayView as a child view:

no one will receive TouchEvent, it is not as efficient as GestureOverlayView is,

since we actually let all events get passed but what we do is we literally kill them

when they are passing through our filter ;).


the last mode is Dynamic mode,

the primary purpose of this mode is to have a bit smarter gesture detection, i mean there has been sometimes that i wanted to slide from one page to another, but a button get pressed and something else happens.

 

it does not happen so much but it is really annoying. what i tried to do in Dynamic mode is to distinguish between a swipe/double tap gesture and a movement which is neither of them.

 

so if you have a view full of buttons and other interactive stuff and user does a swipe or double tap gesture, it is guaranteed (although i believe there is no such thing as guarantee in life ;) )

that no other event callback will be called but only onSwipe() or onDoubleTap().


Anyway that's what i came up with to take the pain away in those circumstances

when we just need to handle some simple Gestures.


hope it will be helpful for you and can make your life a bit easier.

 

http://android-journey.blogspot.com/2010/01/android-gestures.html

 

 

Gesture Builder APK

http://code.google.com/p/quickdroid/downloads/detail?name=com.android.gesture.builder.apk&can=2&q=

 

Source

 /*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.android.gesture.builder

import _root_.android.app.{Dialog, AlertDialog, ListActivity}
import _root_.android.app.Activity._
import _root_.android.os.{AsyncTask, Bundle, Environment}
import _root_.android.view.{ContextMenu, View, MenuItem, LayoutInflater, ViewGroup}
import _root_.android.gesture.{Gesture, GestureLibrary, GestureLibraries}
import _root_.android.widget.{AdapterView, ArrayAdapter, EditText, TextView, Toast}
import _root_.android.content.{Context, DialogInterface, Intent}
import _root_.android.content.res.Resources
import _root_.android.text.TextUtils
import _root_.android.graphics.Bitmap
import _root_.android.graphics.drawable.{BitmapDrawable, Drawable}

import java.util.Comparator
import java.io.File

import scala.collection.JavaConversions.{JListWrapper, JSetWrapper}
import scala.collection.mutable.{HashMap, SynchronizedMap}

object GestureBuilderActivity {
 
private final val STATUS_SUCCESS = 0
 
private final val STATUS_CANCELLED = 1
 
private final val STATUS_NO_STORAGE = 2
 
private final val STATUS_NOT_LOADED = 3

 
private final val MENU_ID_RENAME = 1
 
private final val MENU_ID_REMOVE = 2

 
private final val DIALOG_RENAME_GESTURE = 1

 
private final val REQUEST_NEW_GESTURE = 1
   
 
// Type: long (id)
 
private final val GESTURES_INFO_ID = "gestures.info_id"

 
private final val mStoreFile =
   
new File(Environment.getExternalStorageDirectory, "gestures")
 
private final val sStore = GestureLibraries.fromFile(mStoreFile)

 
def getStore: GestureLibrary = sStore
 
 
class NamedGesture(var name: String, var gesture: Gesture)
}

class GestureBuilderActivity extends ListActivity {
 
import GestureBuilderActivity._  // companion object

 
private final val mSorter = new Comparator[NamedGesture]() {
   
def compare(object1: NamedGesture, object2: NamedGesture): Int = {
      object1
.name compareTo object2.name
   
}
 
}

 
private var mAdapter: GesturesAdapter = _
 
private var mTask: GesturesLoadTask = _
 
private var mEmpty: TextView = _

 
private var mRenameDialog: Dialog = _
 
private var mInput: EditText = _
 
private var mCurrentRenameGesture: NamedGesture = _

 
override protected def onCreate(savedInstanceState: Bundle) {
   
super.onCreate(savedInstanceState)

    setContentView
(R.layout.gestures_list)

    mAdapter
= new GesturesAdapter(this)
    setListAdapter
(mAdapter)

    mEmpty
= findViewById(android.R.id.empty).asInstanceOf[TextView]
    loadGestures
()

    registerForContextMenu
(getListView)
 
}

 
//@SuppressWarnings({"UnusedDeclaration"})
 
def reloadGestures(v: View) {
    loadGestures
()
 
}

 
//@SuppressWarnings({"UnusedDeclaration"})
 
def addGesture(v: View) {
    val intent
= new Intent(this, classOf[CreateGestureActivity])
    startActivityForResult
(intent, REQUEST_NEW_GESTURE)
 
}

 
override protected def onActivityResult(requestCode: Int,
                                          resultCode
: Int, data: Intent) {
   
super.onActivityResult(requestCode, resultCode, data)
       
   
if (resultCode == RESULT_OK) {
      requestCode match
{
       
case REQUEST_NEW_GESTURE =>
          loadGestures
()
     
}
   
}
 
}

 
private def loadGestures() {
   
if (mTask != null && mTask.getStatus != AsyncTask.Status.FINISHED) {
      mTask cancel
true
   
}        
    mTask
= new GesturesLoadTask().execute().asInstanceOf[GesturesLoadTask]
 
}

 
override protected def onDestroy() {
   
super.onDestroy()

   
if (mTask != null && mTask.getStatus != AsyncTask.Status.FINISHED) {
      mTask
.cancel(true)
      mTask
= null
   
}

    cleanupRenameDialog
()
 
}

 
private def checkForEmpty() {
   
if (mAdapter.getCount == 0) {
      mEmpty setText R
.string.gestures_empty
   
}
 
}

 
override protected def onSaveInstanceState(outState: Bundle) {
   
super.onSaveInstanceState(outState)

   
if (mCurrentRenameGesture != null) {
      outState
.putLong(GESTURES_INFO_ID, mCurrentRenameGesture.gesture.getID)
   
}
 
}

 
override protected def onRestoreInstanceState(state: Bundle) {
   
super.onRestoreInstanceState(state)

    val id
= state.getLong(GESTURES_INFO_ID, -1)
   
if (id != -1) {
      val entries
= new JSetWrapper(sStore.getGestureEntries)
     
var found = false
     
for (name <- entries if !found) {
        val gestures
= new JListWrapper(sStore.getGestures(name))
       
for (gesture <- gestures if !found) {
         
if (gesture.getID == id) {
            mCurrentRenameGesture
= new NamedGesture(name, gesture)
            found
= true
         
}
       
}
     
}
   
}
 
}

 
override def onCreateContextMenu(menu: ContextMenu, v: View,
                                   menuInfo
: ContextMenu.ContextMenuInfo) {
   
super.onCreateContextMenu(menu, v, menuInfo)

    val info
= menuInfo.asInstanceOf[AdapterView.AdapterContextMenuInfo]
    menu setHeaderTitle info
.targetView.asInstanceOf[TextView].getText

    menu
.add(0, MENU_ID_RENAME, 0, R.string.gestures_rename)
    menu
.add(0, MENU_ID_REMOVE, 0, R.string.gestures_delete)
 
}

 
override def onContextItemSelected(item: MenuItem): Boolean = {
    val menuInfo
=
      item
.getMenuInfo.asInstanceOf[AdapterView.AdapterContextMenuInfo]
    val gesture
= menuInfo.targetView.getTag.asInstanceOf[NamedGesture]

    item
.getItemId match {
     
case MENU_ID_RENAME =>
        renameGesture
(gesture)
       
true
     
case MENU_ID_REMOVE =>
        deleteGesture
(gesture)
       
true
     
case _ =>
       
super.onContextItemSelected(item)
   
}
 
}

 
private def renameGesture(gesture: NamedGesture) {
    mCurrentRenameGesture
= gesture
    showDialog
(DIALOG_RENAME_GESTURE)
 
}

 
override protected def onCreateDialog(id: Int): Dialog = {
   
if (id == DIALOG_RENAME_GESTURE)
      createRenameDialog
()
   
else
     
super.onCreateDialog(id)
 
}

 
override protected def onPrepareDialog(id: Int, dialog: Dialog) {
   
super.onPrepareDialog(id, dialog)
   
if (id == DIALOG_RENAME_GESTURE) {
      mInput setText mCurrentRenameGesture
.name
   
}
 
}

 
private def createRenameDialog(): Dialog = {
    val layout
= View.inflate(this, R.layout.dialog_rename, null)
    mInput
= layout.findViewById(R.id.name).asInstanceOf[EditText]
    val tv
= layout.findViewById(R.id.label).asInstanceOf[TextView]
    tv setText R
.string.gestures_rename_label

    val builder
= new AlertDialog.Builder(this)
    builder setIcon
0
    builder setTitle getString
(R.string.gestures_rename_title)
    builder setCancelable
true
    builder setOnCancelListener
new /*Dialog*/DialogInterface.OnCancelListener {
     
def onCancel(dialog: DialogInterface) {
        cleanupRenameDialog
()
     
}
   
}
    builder
.setNegativeButton(getString(R.string.cancel_action),
     
new /*Dialog*/DialogInterface.OnClickListener {
       
def onClick(dialog: DialogInterface, which: Int) {
          cleanupRenameDialog
()
       
}
     
}
   
)
    builder
.setPositiveButton(getString(R.string.rename_action),
     
new /*Dialog*/DialogInterface.OnClickListener() {
       
def onClick(dialog: DialogInterface, which: Int) {
          changeGestureName
()
       
}
     
}
   
)
    builder setView layout
    builder
.create()
 
}

 
private def changeGestureName() {
    val name
= mInput.getText.toString
   
if (!TextUtils.isEmpty(name)) {
      val renameGesture
= mCurrentRenameGesture
      val adapter
= mAdapter
      val count
= adapter.getCount

     
// Simple linear search, there should not be enough items to warrant
     
// a more sophisticated search
     
var found = false
     
for (i <- 0 until count if !found) {
        val gesture
= adapter.getItem(i)
       
if (gesture.gesture.getID == renameGesture.gesture.getID) {
          sStore
.removeGesture(gesture.name, gesture.gesture)
          gesture
.name = mInput.getText.toString
          sStore
.addGesture(gesture.name, gesture.gesture)
          found
= true
       
}
     
}

      adapter
.notifyDataSetChanged()
   
}
    mCurrentRenameGesture
= null
 
}

 
private def cleanupRenameDialog() {
   
if (mRenameDialog != null) {
      mRenameDialog
.dismiss()
      mRenameDialog
= null
   
}
    mCurrentRenameGesture
= null
 
}

 
private def deleteGesture(gesture: NamedGesture) {
    sStore
.removeGesture(gesture.name, gesture.gesture)
    sStore
.save()

    val adapter
= mAdapter
    adapter setNotifyOnChange
false
    adapter remove gesture
    adapter sort mSorter
    checkForEmpty
()
    adapter
.notifyDataSetChanged()

   
Toast.makeText(this, R.string.gestures_delete_success,
                   
Toast.LENGTH_SHORT).show()
 
}

 
private class GesturesLoadTask extends AsyncTask[AnyRef, NamedGesture, Int] {
   
private var mThumbnailSize: Int = _
   
private var mThumbnailInset: Int = _
   
private var mPathColor: Int = _

   
override protected def onPreExecute() {
     
super.onPreExecute()

      val resources
= getResources
      mPathColor
= resources getColor R.color.gesture_color
      mThumbnailInset
=
        resources
.getDimension(R.dimen.gesture_thumbnail_inset).toInt
      mThumbnailSize
=
        resources
.getDimension(R.dimen.gesture_thumbnail_size).toInt

      findViewById
(R.id.addButton) setEnabled false
      findViewById
(R.id.reloadButton) setEnabled false
           
      mAdapter setNotifyOnChange
false          
      mAdapter
.clear()
   
}

   
// temporary workaround (compiler bug !?)
   
private def publishProgress1(values: NamedGesture*) {
     
super.publishProgress(values: _*)
   
}

   
override protected def doInBackground(params: AnyRef*): Int = {
     
if (isCancelled())
        STATUS_CANCELLED
     
else if (! (Environment.MEDIA_MOUNTED equals
                 
Environment.getExternalStorageState))
        STATUS_NO_STORAGE
     
else if (sStore.load()) {
        val entries
= new JSetWrapper(sStore.getGestureEntries)
       
for (name <- entries if !isCancelled()) {
          val gestures
= new JListWrapper(sStore.getGestures(name))
         
for (gesture <- gestures) {
            val bitmap
= gesture.toBitmap(mThumbnailSize, mThumbnailSize,
                                          mThumbnailInset
, mPathColor)
            val namedGesture
= new NamedGesture(name, gesture)

            mAdapter
.addBitmap(namedGesture.gesture.getID, bitmap)
            publishProgress1
(namedGesture)
         
}
       
}

        STATUS_SUCCESS
     
} else
        STATUS_NOT_LOADED
   
}

   
override protected def onProgressUpdate(values: NamedGesture*) {
     
super.onProgressUpdate(values: _*)

      val adapter
= mAdapter
      adapter setNotifyOnChange
false

     
for (gesture <- values) {
        adapter add gesture
     
}

      adapter sort mSorter
     
      adapter
.notifyDataSetChanged()
   
}

   
override protected def onPostExecute(result: Int) {
     
super.onPostExecute(result)

     
if (result == STATUS_NO_STORAGE) {
        getListView setVisibility
View.GONE
        mEmpty setVisibility
View.VISIBLE
        mEmpty setText getString
(R.string.gestures_error_loading,
                                 mStoreFile
.getAbsolutePath)
     
} else {
        findViewById
(R.id.addButton) setEnabled true
        findViewById
(R.id.reloadButton) setEnabled true
        checkForEmpty
()
     
}
   
}
 
}

 
private class GesturesAdapter(context: Context)
 
extends ArrayAdapter[NamedGesture](context, 0) {
   
private val mInflater: LayoutInflater =
      context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
   
private val mThumbnails =
     
new HashMap[Long, Drawable] with SynchronizedMap[Long, Drawable]

   
def addBitmap(id: Long, bitmap: Bitmap) {
      mThumbnails
(id) = new BitmapDrawable(bitmap)
   
}

   
override def getView(position: Int, convertView: View,
                         parent
: ViewGroup): View = {
      val convertView1
= if (convertView == null)
        mInflater
.inflate(R.layout.gestures_item, parent, false)
     
else
        conve

안드로이드 제스쳐

 

 

 

 

 

 

 

 

 

reating a gestures library
 
Loading the gestures library
 
Recognizing gestures
 
Gestures overlay
 
==================== 

 
Like it or not touch screens are becoming part of both developers and users life, i mean dont think i would buy one of Those uncool phones without touchscreen unless i really have to, would you? but the important point is that what is the Touchscreen use if applications doesn't support touchscreen interaction, in other words who is really gonna pay for a ,say, Picture management application if it does not support some gestures for switching between pictures or zooming? [ Gesture Detecti
on in Android ].
In Android there are three levels of touch screen event handling mechanism which can be used by developers. the most low level technique is to receive all touch events and take care of all things, you can attach an 'OnTouchListener' to a view and get notified whenever there is a touch event or you can override onTouchEvent() or dispatchTouchEvent() method of your activity or view, in all these cases you would be dealing with an instance of MotionEvent every single time and you would have to detect what user is doing all on your own which will suit requirments for developing games and stuff like that. But it is just too much hassle if you only need a few simple gestures for your application.

Next approach is to use GestureDetector class along with OnGestureListener and/or OnDoubleTapListener, in this technique whenever there is a new MotionEvent you have to pass it to Gesture Detector's onTouchEvent() method, it then will analyse this event and previous events and tell you what is happening on the screen by calling some of the callback methods.

here is a simple activity which uses GestureDetector :

 public class SimpleActivity extends Activity
 implements OnDoubleTapListener,
  android.view.GestureDetector.OnGestureListener { 

 private GestureDetector detector;     
 /** Called when the activity is first created. */   
 @Override   
 public void onCreate(Bundle savedInstanceState)
 {    
  super.onCreate(savedInstanceState);       
  setContentView(R.layout.simple);                
  detector = new GestureDetector(this,this);   
  
 }    
 @Override 
 public boolean onTouchEvent(MotionEvent me)
 {   
  this.detector.onTouchEvent(me); 
  return super.onTouchEvent(me); 
  
 }
 //@Override
 public boolean onDown(MotionEvent e)
 {   
  Log.d("---onDown----",e.toString());    
  return false;
  
 }
 //@Override
 public boolean onFling(MotionEvent e1,
   MotionEvent e2, float velocityX,  
   float velocityY)
 { 
  Log.d("---onFling---",e1.toString()+e2.toString());  
  return false;
  
 }
 //@Override
 public void onLongPress(MotionEvent e)
 { 
  Log.d("---onLongPress---",e.toString()); 
  
 }
 //@Override
 public boolean onScroll(MotionEvent e1,
   MotionEvent e2, float distanceX,   float distanceY)
 { 
  Log.d("---onScroll---",e1.toString()+e2.toString()); 
  return false;
  
 }
 //@Override
 public void onShowPress(MotionEvent e)
 { 
  Log.d("---onShowPress---",e.toString());
  
 }
 //@Override
 public boolean onSingleTapUp(MotionEvent e)
 { 
  Log.d("---onSingleTapUp---",e.toString()); 
  return false;
  
 }
 //@Override
 public boolean onDoubleTap(MotionEvent e)
 { 
  Log.d("---onDoubleTap---",e.toString()); 
  return false;
  
 }
 //@Override
 public boolean onDoubleTapEvent(MotionEvent e)
 { 
  Log.d("---onDoubleTapEvent---",e.toString());  
  return false;
  
 }
 //@Override
 public boolean onSingleTapConfirmed(MotionEvent e)
 { 
  Log.d("---onSingleTapConfirmed---",e.toString()); 
  return false;
  
 }

 
}

 
if we want to understand all gesture types and how they work, first of all we need to know three basic MotionEvents which can combine with each other and create some gestures, these three Events are Action_Down , Action_Move and Action_Up , each time you touch the screen an Action_Down occurs and when you start moving it will create Action_Move event and finally when you take your finger off the screen an Action_Up Event will be created.
onDown() is called simply when there is an Action_Down event.
onShowPress() is called after onDown() and before any other callback method, I found out it sometimes might not get called for example when you tap on the screen so fast, but it's actually what this method all about, to make a distinction between a possible unintentional touch and an intentional one.
onSingleTapUp() is called when there is a Tap Gesture. Tap Gesture happens when an Action_Down event followed by an Action_Up event(like a Single-Click).
onDoubleTap() is called when there is two consecutive Tap gesture(like a Double-Click).
onSingleTapConfirmed() is so similar to onSingleTapUp() but it is only get called when the detected tap gesture is definitely a single Tap and not part of a double tap gesture.
Here is the sequence in which these callback methods are called when we tap on the screen:

onDown() – onShowPress() - onSingleTapUp() – onSingleTapConfirmed()


and here is when we do a double tap:


onDown() – onShowPress() - onSingleTapUp() – onDoubleTap() – onDoubleTapEvent()
onDown() – onShowPress() – onDoubleTapEvent()


onFling() is called when a Fling Gesture is detected. fling Gesture occurs when there is an Action_Down then some Action_Move events and finally an Action_Up, but they must take place with a specified movement and velocity pattern to be considered as a fling gesture. for example if you put your finger on the screen and start moving it slowly and then remove your finger gently it won’t be count as a fling gesture.

onScroll() is usually called when there is a Action_Move event so if you put your finger on the screen and move it for a few seconds there will be a method call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... - onScroll()


or If the movement was a Fling Gesture, then there would be a call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... – onFling()

If there is an Action_Move event between first tap and second tap of a doubleTap gesture it will be handled by calling onDoubleTapEvent() instead of onScroll() method. onDoubleTapEvent() receives all Action_Up events of a doubleTap gesture as well.

Remember that if we touch the screen and don’t remove our finger for a specified amount of time onLongPress() method is called and in most cases there will be no further event callback regardless of whatever we do after that, moving or removing our finger. we can easily change this behavior of detector by calling setIsLongpressEnabled() method of GestureDetector class.

although GestureDetector makes our life much easier, it still could be a real pain in the ass if we would need to handle some complicated gestures, imagine you need an application which should do task1 when there is a circle gesture, task2 for rectangle gesture and task3 for triangle gesture. obviously it would not be so pleasant to deal with such a situation with those mechanism we have seen so far, it's actually when Gesture API and Gesture Builder Application come into play.

Gesture Builder Application comes with Android emulator as a pre-installed app, It helps us to simply make new gestures and save them on the device, then we can retrieve and detect those gestures later using Gesture API.
 I'm not gonna talk about Gesture Builder app here, 제스처 빌더를 여기서 말하려 하는거 아니다.
since there is a pretty good Article about it on Android Developers blog.
 
// 제스처 오버레이 뷰 클래스
the only thing I'd like to mention here is GestureOverlayView class,
 It is actually just a transparent FrameLayout which can detect gestures but the thing is it can be used in two completely different ways, you can either put other views inside it and use it as a parent view or put it is the last child view of a FrameLayout (or any other way which causes it to be placed on top of another view).
 
//첫번째 시나리오  : 모든 차일드 뷰가 터치 이벤트를 받는다..
In the first Scenario all child views will receive Touch events,
 therefore we will be able to press buttons or interact with other widgets as well as doing some gestures, on the other hand if GestureOverlayView has been placed on top, it will swallow all Touch events and no underlay view will be notified for any touch Event.

Although Gesture API brings many useful features for us,
I personally prefer to use GestureDetector for some simple,
basic gestures and honestly I feel like something is missing here,
I mean , apart from games, I would say more than 70% of all gestures
that might be needed in our applications are just a simple sliding in different directions or a double tap.
and that's why I have decided to come up with something easy
which can enable us to handle those 70% as simple as possible. how simple? you might be asking...

here is how our previous activity will look like if we use SimpleGestureFilter class :

 public class SimpleActivity extends Activity implements SimpleGestureListener{

private SimpleGestureFilter detector;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        detector = new SimpleGestureFilter(this,this);
    }
  
  @Override
public boolean dispatchTouchEvent(MotionEvent me){
   this.detector.onTouchEvent(me);
  return super.dispatchTouchEvent(me);
}

  @Override
public void onSwipe(int direction) {
  String str = "";
 
  switch (direction) {
 
  case SimpleGestureFilter.SWIPE_RIGHT : str = "Swipe Right";
                                           break;
  case SimpleGestureFilter.SWIPE_LEFT :  str = "Swipe Left";
                                                 break;
  case SimpleGestureFilter.SWIPE_DOWN :  str = "Swipe Down";
                                                 break;
  case SimpleGestureFilter.SWIPE_UP :    str = "Swipe Up";
                                                 break;
                                          
  }
   Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}

@Override
public void onDoubleTap() {
    Toast.makeText(this, "Double Tap", Toast.LENGTH_SHORT).show();
}

}

 

and here is SimpleGestureFilter source code :

 public class SimpleGestureFilter extends SimpleOnGestureListener{
   
public final static int SWIPE_UP    = 1;
public final static int SWIPE_DOWN  = 2;
public final static int SWIPE_LEFT  = 3;
public final static int SWIPE_RIGHT = 4;

public final static int MODE_TRANSPARENT = 0;
public final static int MODE_SOLID       = 1;
public final static int MODE_DYNAMIC     = 2;

private final static int ACTION_FAKE = -13; //just an unlikely number
private int swipe_Min_Distance = 100;
private int swipe_Max_Distance = 350;
private int swipe_Min_Velocity = 100;

private int mode      = MODE_DYNAMIC;
private boolean running = true;
private boolean tapIndicator = false;

private Activity context;
private GestureDetector detector;
private SimpleGestureListener listener;


public SimpleGestureFilter(Activity context,SimpleGestureListener sgl) {

  this.context = context;
  this.detector = new GestureDetector(context, this);
  this.listener = sgl;
}

public void onTouchEvent(MotionEvent event){
 
   if(!this.running)
  return; 
 
   boolean result = this.detector.onTouchEvent(event);
 
   if(this.mode == MODE_SOLID)
    event.setAction(MotionEvent.ACTION_CANCEL);
   else if (this.mode == MODE_DYNAMIC) {
 
     if(event.getAction() == ACTION_FAKE)
       event.setAction(MotionEvent.ACTION_UP);
     else if (result)
       event.setAction(MotionEvent.ACTION_CANCEL);
     else if(this.tapIndicator){
      event.setAction(MotionEvent.ACTION_DOWN);
      this.tapIndicator = false;
     }
 
   }
   //else just do nothing, it's Transparent
}

public void setMode(int m){
  this.mode = m;
}

public int getMode(){
  return this.mode;
}

public void setEnabled(boolean status){
  this.running = status;
}

public void setSwipeMaxDistance(int distance){
  this.swipe_Max_Distance = distance;
}

public void setSwipeMinDistance(int distance){
  this.swipe_Min_Distance = distance;
}

public void setSwipeMinVelocity(int distance){
  this.swipe_Min_Velocity = distance;
}

public int getSwipeMaxDistance(){
  return this.swipe_Max_Distance;
}

public int getSwipeMinDistance(){
  return this.swipe_Min_Distance;
}

public int getSwipeMinVelocity(){
  return this.swipe_Min_Velocity;
}


@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
   float velocityY) {

  final float xDistance = Math.abs(e1.getX() - e2.getX());
  final float yDistance = Math.abs(e1.getY() - e2.getY());

  if(xDistance > this.swipe_Max_Distance || yDistance > this.swipe_Max_Distance)
   return false;

  velocityX = Math.abs(velocityX);
  velocityY = Math.abs(velocityY);
        boolean result = false;

if(velocityX > this.swipe_Min_Velocity && xDistance > this.swipe_Min_Distance){
   if(e1.getX() > e2.getX()) // right to left
    this.listener.onSwipe(SWIPE_LEFT);
   else
    this.listener.onSwipe(SWIPE_RIGHT);
  
   result = true;
  }
  else if(velocityY > this.swipe_Min_Velocity && yDistance > this.swipe_Min_Distance){
   if(e1.getY() > e2.getY()) // bottom to up
    this.listener.onSwipe(SWIPE_UP);
   else
    this.listener.onSwipe(SWIPE_DOWN);
  
   result = true;
  }

   return result;
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
  this.tapIndicator = true;
  return false;
}

@Override
public boolean onDoubleTap(MotionEvent arg0) {
  this.listener.onDoubleTap();;
  return true;
}

@Override
public boolean onDoubleTapEvent(MotionEvent arg0) {
  return true;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent arg0) {
 
  if(this.mode == MODE_DYNAMIC){        // we owe an ACTION_UP, so we fake an      
     arg0.setAction(ACTION_FAKE);      //action which will be converted to an ACTION_UP later.                                   
     this.context.dispatchTouchEvent(arg0); 
  }  
    
  return false;
}


    static interface SimpleGestureListener{
     void onSwipe(int direction);
     void onDoubleTap();
}

}

 

as you can see clients of these class can determine the minimum and maximum distance and also minimum velocity which is required for a movement on screen to be considered as a Swipe Gesture,

 

I also thought it would be great if our filter can behave differently like what GestureOverlayView

can do and even more than that!


this Filter can run in three different mode:

Transparent, Solid and Dynamic.

 

in Transparent mode

it will work just like when we have a GestureOverlayView as parent: all views will receive Touch events;

Solid mode works

like when we put a GestureOverlayView as a child view:

no one will receive TouchEvent, it is not as efficient as GestureOverlayView is,

since we actually let all events get passed but what we do is we literally kill them

when they are passing through our filter ;).


the last mode is Dynamic mode,

the primary purpose of this mode is to have a bit smarter gesture detection, i mean there has been sometimes that i wanted to slide from one page to another, but a button get pressed and something else happens.

 

it does not happen so much but it is really annoying. what i tried to do in Dynamic mode is to distinguish between a swipe/double tap gesture and a movement which is neither of them.

 

so if you have a view full of buttons and other interactive stuff and user does a swipe or double tap gesture, it is guaranteed (although i believe there is no such thing as guarantee in life ;) )

that no other event callback will be called but only onSwipe() or onDoubleTap().


Anyway that's what i came up with to take the pain away in those circumstances

when we just need to handle some simple Gestures.


hope it will be helpful for you and can make your life a bit easier.

 

http://android-journey.blogspot.com/2010/01/android-gestures.html

 

 

Gesture Builder APK

http://code.google.com/p/quickdroid/downloads/detail?name=com.android.gesture.builder.apk&can=2&q=

 

Source

 /*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.android.gesture.builder

import _root_.android.app.{Dialog, AlertDialog, ListActivity}
import _root_.android.app.Activity._
import _root_.android.os.{AsyncTask, Bundle, Environment}
import _root_.android.view.{ContextMenu, View, MenuItem, LayoutInflater, ViewGroup}
import _root_.android.gesture.{Gesture, GestureLibrary, GestureLibraries}
import _root_.android.widget.{AdapterView, ArrayAdapter, EditText, TextView, Toast}
import _root_.android.content.{Context, DialogInterface, Intent}
import _root_.android.content.res.Resources
import _root_.android.text.TextUtils
import _root_.android.graphics.Bitmap
import _root_.android.graphics.drawable.{BitmapDrawable, Drawable}

import java.util.Comparator
import java.io.File

import scala.collection.JavaConversions.{JListWrapper, JSetWrapper}
import scala.collection.mutable.{HashMap, SynchronizedMap}

object GestureBuilderActivity {
 
private final val STATUS_SUCCESS = 0
 
private final val STATUS_CANCELLED = 1
 
private final val STATUS_NO_STORAGE = 2
 
private final val STATUS_NOT_LOADED = 3

 
private final val MENU_ID_RENAME = 1
 
private final val MENU_ID_REMOVE = 2

 
private final val DIALOG_RENAME_GESTURE = 1

 
private final val REQUEST_NEW_GESTURE = 1
   
 
// Type: long (id)
 
private final val GESTURES_INFO_ID = "gestures.info_id"

 
private final val mStoreFile =
   
new File(Environment.getExternalStorageDirectory, "gestures")
 
private final val sStore = GestureLibraries.fromFile(mStoreFile)

 
def getStore: GestureLibrary = sStore
 
 
class NamedGesture(var name: String, var gesture: Gesture)
}

class GestureBuilderActivity extends ListActivity {
 
import GestureBuilderActivity._  // companion object

 
private final val mSorter = new Comparator[NamedGesture]() {
   
def compare(object1: NamedGesture, object2: NamedGesture): Int = {
      object1
.name compareTo object2.name
   
}
 
}

 
private var mAdapter: GesturesAdapter = _
 
private var mTask: GesturesLoadTask = _
 
private var mEmpty: TextView = _

 
private var mRenameDialog: Dialog = _
 
private var mInput: EditText = _
 
private var mCurrentRenameGesture: NamedGesture = _

 
override protected def onCreate(savedInstanceState: Bundle) {
   
super.onCreate(savedInstanceState)

    setContentView
(R.layout.gestures_list)

    mAdapter
= new GesturesAdapter(this)
    setListAdapter
(mAdapter)

    mEmpty
= findViewById(android.R.id.empty).asInstanceOf[TextView]
    loadGestures
()

    registerForContextMenu
(getListView)
 
}

 
//@SuppressWarnings({"UnusedDeclaration"})
 
def reloadGestures(v: View) {
    loadGestures
()
 
}

 
//@SuppressWarnings({"UnusedDeclaration"})
 
def addGesture(v: View) {
    val intent
= new Intent(this, classOf[CreateGestureActivity])
    startActivityForResult
(intent, REQUEST_NEW_GESTURE)
 
}

 
override protected def onActivityResult(requestCode: Int,
                                          resultCode
: Int, data: Intent) {
   
super.onActivityResult(requestCode, resultCode, data)
       
   
if (resultCode == RESULT_OK) {
      requestCode match
{
       
case REQUEST_NEW_GESTURE =>
          loadGestures
()
     
}
   
}
 
}

 
private def loadGestures() {
   
if (mTask != null && mTask.getStatus != AsyncTask.Status.FINISHED) {
      mTask cancel
true
   
}        
    mTask
= new GesturesLoadTask().execute().asInstanceOf[GesturesLoadTask]
 
}

 
override protected def onDestroy() {
   
super.onDestroy()

   
if (mTask != null && mTask.getStatus != AsyncTask.Status.FINISHED) {
      mTask
.cancel(true)
      mTask
= null
   
}

    cleanupRenameDialog
()
 
}

 
private def checkForEmpty() {
   
if (mAdapter.getCount == 0) {
      mEmpty setText R
.string.gestures_empty
   
}
 
}

 
override protected def onSaveInstanceState(outState: Bundle) {
   
super.onSaveInstanceState(outState)

   
if (mCurrentRenameGesture != null) {
      outState
.putLong(GESTURES_INFO_ID, mCurrentRenameGesture.gesture.getID)
   
}
 
}

 
override protected def onRestoreInstanceState(state: Bundle) {
   
super.onRestoreInstanceState(state)

    val id
= state.getLong(GESTURES_INFO_ID, -1)
   
if (id != -1) {
      val entries
= new JSetWrapper(sStore.getGestureEntries)
     
var found = false
     
for (name <- entries if !found) {
        val gestures
= new JListWrapper(sStore.getGestures(name))
       
for (gesture <- gestures if !found) {
         
if (gesture.getID == id) {
            mCurrentRenameGesture
= new NamedGesture(name, gesture)
            found
= true
         
}
       
}
     
}
   
}
 
}

 
override def onCreateContextMenu(menu: ContextMenu, v: View,
                                   menuInfo
: ContextMenu.ContextMenuInfo) {
   
super.onCreateContextMenu(menu, v, menuInfo)

    val info
= menuInfo.asInstanceOf[AdapterView.AdapterContextMenuInfo]
    menu setHeaderTitle info
.targetView.asInstanceOf[TextView].getText

    menu
.add(0, MENU_ID_RENAME, 0, R.string.gestures_rename)
    menu
.add(0, MENU_ID_REMOVE, 0, R.string.gestures_delete)
 
}

 
override def onContextItemSelected(item: MenuItem): Boolean = {
    val menuInfo
=
      item
.getMenuInfo.asInstanceOf[AdapterView.AdapterContextMenuInfo]
    val gesture
= menuInfo.targetView.getTag.asInstanceOf[NamedGesture]

    item
.getItemId match {
     
case MENU_ID_RENAME =>
        renameGesture
(gesture)
       
true
     
case MENU_ID_REMOVE =>
        deleteGesture
(gesture)
       
true
     
case _ =>
       
super.onContextItemSelected(item)
   
}
 
}

 
private def renameGesture(gesture: NamedGesture) {
    mCurrentRenameGesture
= gesture
    showDialog
(DIALOG_RENAME_GESTURE)
 
}

 
override protected def onCreateDialog(id: Int): Dialog = {
   
if (id == DIALOG_RENAME_GESTURE)
      createRenameDialog
()
   
else
     
super.onCreateDialog(id)
 
}

 
override protected def onPrepareDialog(id: Int, dialog: Dialog) {
   
super.onPrepareDialog(id, dialog)
   
if (id == DIALOG_RENAME_GESTURE) {
      mInput setText mCurrentRenameGesture
.name
   
}
 
}

 
private def createRenameDialog(): Dialog = {
    val layout
= View.inflate(this, R.layout.dialog_rename, null)
    mInput
= layout.findViewById(R.id.name).asInstanceOf[EditText]
    val tv
= layout.findViewById(R.id.label).asInstanceOf[TextView]
    tv setText R
.string.gestures_rename_label

    val builder
= new AlertDialog.Builder(this)
    builder setIcon
0
    builder setTitle getString
(R.string.gestures_rename_title)
    builder setCancelable
true
    builder setOnCancelListener
new /*Dialog*/DialogInterface.OnCancelListener {
     
def onCancel(dialog: DialogInterface) {
        cleanupRenameDialog
()
     
}
   
}
    builder
.setNegativeButton(getString(R.string.cancel_action),
     
new /*Dialog*/DialogInterface.OnClickListener {
       
def onClick(dialog: DialogInterface, which: Int) {
          cleanupRenameDialog
()
       
}
     
}
   
)
    builder
.setPositiveButton(getString(R.string.rename_action),
     
new /*Dialog*/DialogInterface.OnClickListener() {
       
def onClick(dialog: DialogInterface, which: Int) {
          changeGestureName
()
       
}
     
}
   
)
    builder setView layout
    builder
.create()
 
}

 
private def changeGestureName() {
    val name
= mInput.getText.toString
   
if (!TextUtils.isEmpty(name)) {
      val renameGesture
= mCurrentRenameGesture
      val adapter
= mAdapter
      val count
= adapter.getCount

     
// Simple linear search, there should not be enough items to warrant
     
// a more sophisticated search
     
var found = false
     
for (i <- 0 until count if !found) {
        val gesture
= adapter.getItem(i)
       
if (gesture.gesture.getID == renameGesture.gesture.getID) {
          sStore
.removeGesture(gesture.name, gesture.gesture)
          gesture
.name = mInput.getText.toString
          sStore
.addGesture(gesture.name, gesture.gesture)
          found
= true
       
}
     
}

      adapter
.notifyDataSetChanged()
   
}
    mCurrentRenameGesture
= null
 
}

 
private def cleanupRenameDialog() {
   
if (mRenameDialog != null) {
      mRenameDialog
.dismiss()
      mRenameDialog
= null
   
}
    mCurrentRenameGesture
= null
 
}

 
private def deleteGesture(gesture: NamedGesture) {
    sStore
.removeGesture(gesture.name, gesture.gesture)
    sStore
.save()

    val adapter
= mAdapter
    adapter setNotifyOnChange
false
    adapter remove gesture
    adapter sort mSorter
    checkForEmpty
()
    adapter
.notifyDataSetChanged()

   
Toast.makeText(this, R.string.gestures_delete_success,
                   
Toast.LENGTH_SHORT).show()
 
}

 
private class GesturesLoadTask extends AsyncTask[AnyRef, NamedGesture, Int] {
   
private var mThumbnailSize: Int = _
   
private var mThumbnailInset: Int = _
   
private var mPathColor: Int = _

   
override protected def onPreExecute() {
     
super.onPreExecute()

      val resources
= getResources
      mPathColor
= resources getColor R.color.gesture_color
      mThumbnailInset
=
        resources
.getDimension(R.dimen.gesture_thumbnail_inset).toInt
      mThumbnailSize
=
        resources
.getDimension(R.dimen.gesture_thumbnail_size).toInt

      findViewById
(R.id.addButton) setEnabled false
      findViewById
(R.id.reloadButton) setEnabled false
           
      mAdapter setNotifyOnChange
false          
      mAdapter
.clear()
   
}

   
// temporary workaround (compiler bug !?)
   
private def publishProgress1(values: NamedGesture*) {
     
super.publishProgress(values: _*)
   
}

   
override protected def doInBackground(params: AnyRef*): Int = {
     
if (isCancelled())
        STATUS_CANCELLED
     
else if (! (Environment.MEDIA_MOUNTED equals
                 
Environment.getExternalStorageState))
        STATUS_NO_STORAGE
     
else if (sStore.load()) {
        val entries
= new JSetWrapper(sStore.getGestureEntries)
       
for (name <- entries if !isCancelled()) {
          val gestures
= new JListWrapper(sStore.getGestures(name))
         
for (gesture <- gestures) {
            val bitmap
= gesture.toBitmap(mThumbnailSize, mThumbnailSize,
                                          mThumbnailInset
, mPathColor)
            val namedGesture
= new NamedGesture(name, gesture)

            mAdapter
.addBitmap(namedGesture.gesture.getID, bitmap)
            publishProgress1
(namedGesture)
         
}
       
}

        STATUS_SUCCESS
     
} else
        STATUS_NOT_LOADED
   
}

   
override protected def onProgressUpdate(values: NamedGesture*) {
     
super.onProgressUpdate(values: _*)

      val adapter
= mAdapter
      adapter setNotifyOnChange
false

     
for (gesture <- values) {
        adapter add gesture
     
}

      adapter sort mSorter
     
      adapter
.notifyDataSetChanged()
   
}

   
override protected def onPostExecute(result: Int) {
     
super.onPostExecute(result)

     
if (result == STATUS_NO_STORAGE) {
        getListView setVisibility
View.GONE
        mEmpty setVisibility
View.VISIBLE
        mEmpty setText getString
(R.string.gestures_error_loading,
                                 mStoreFile
.getAbsolutePath)
     
} else {
        findViewById
(R.id.addButton) setEnabled true
        findViewById
(R.id.reloadButton) setEnabled true
        checkForEmpty
()
     
}
   
}
 
}

 
private class GesturesAdapter(context: Context)
 
extends ArrayAdapter[NamedGesture](context, 0) {
   
private val mInflater: LayoutInflater =
      context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
   
private val mThumbnails =
     
new HashMap[Long, Drawable] with SynchronizedMap[Long, Drawable]

   
def addBitmap(id: Long, bitmap: Bitmap) {
      mThumbnails
(id) = new BitmapDrawable(bitmap)
   
}

   
override def getView(position: Int, convertView: View,
                         parent
: ViewGroup): View = {
      val convertView1
= if (convertView == null)
        mInflater
.inflate(R.layout.gestures_item, parent, false)
     
else
        convertView

      val gesture
= getItem(position)
      val label
= convertView1.asInstanceOf[TextView]

      label setTag gesture
      label setText gesture
.name
      label
.setCompoundDrawablesWithIntrinsicBounds(
         mThumbnails
(gesture.gesture.getID), null, null, null)

      convertView1
   
}
 
}
}

http://www.assembla.com/code/scala-eclipse-toolchain/git/nodes/docs/android-examples/GestureBuilder/src/com/android/gesture/builder/GestureBuilderActivity.scala?rev=334c117e3292c12e728639de6c19056b3513d877 rtView

      val gesture = getItem(position)
      val label
= convertView1.asInstanceOf[TextView]

      label setTag gesture
      label setText gesture
.name
      label
.setCompoundDrawablesWithIntrinsicBounds(
         mThumbnails
(gesture.gesture.getID), null, null, null)

      convertView1
   
}
 
}
}

http://www.assembla.com/code/scala-eclipse-toolchain/git/nodes/docs/android-examples/GestureBuilder/src/com/android/gesture/builder/GestureBuilderActivity.scala?rev=334c117e3292c12e728639de6c19056b3513d877 

 

 

 

 

 

 

 

 

 

reating a gestures library
 
Loading the gestures library
 
Recognizing gestures
 
Gestures overlay
 
==================== 
 
Like it or not touch screens are becoming part of both developers and users life, i mean dont think i would buy one of Those uncool phones without touchscreen unless i really have to, would you? but the important point is that what is the Touchscreen use if applications doesn't support touchscreen interaction, in other words who is really gonna pay for a ,say, Picture management application if it does not support some gestures for switching between pictures or zooming? [ Gesture Detecti
on in Android ].
In Android there are three levels of touch screen event handling mechanism which can be used by developers. the most low level technique is to receive all touch events and take care of all things, you can attach an 'OnTouchListener' to a view and get notified whenever there is a touch event or you can override onTouchEvent() or dispatchTouchEvent() method of your activity or view, in all these cases you would be dealing with an instance of MotionEvent every single time and you would have to detect what user is doing all on your own which will suit requirments for developing games and stuff like that. But it is just too much hassle if you only need a few simple gestures for your application.

Next approach is to use GestureDetector class along with OnGestureListener and/or OnDoubleTapListener, in this technique whenever there is a new MotionEvent you have to pass it to Gesture Detector's onTouchEvent() method, it then will analyse this event and previous events and tell you what is happening on the screen by calling some of the callback methods.

here is a simple activity which uses GestureDetector :

 public class SimpleActivity extends Activity
 implements OnDoubleTapListener,
  android.view.GestureDetector.OnGestureListener { 

 private GestureDetector detector;     
 /** Called when the activity is first created. */   
 @Override   
 public void onCreate(Bundle savedInstanceState)
 {    
  super.onCreate(savedInstanceState);       
  setContentView(R.layout.simple);                
  detector = new GestureDetector(this,this);   
  
 }    
 @Override 
 public boolean onTouchEvent(MotionEvent me)
 {   
  this.detector.onTouchEvent(me); 
  return super.onTouchEvent(me); 
  
 }
 //@Override
 public boolean onDown(MotionEvent e)
 {   
  Log.d("---onDown----",e.toString());    
  return false;
  
 }
 //@Override
 public boolean onFling(MotionEvent e1,
   MotionEvent e2, float velocityX,  
   float velocityY)
 { 
  Log.d("---onFling---",e1.toString()+e2.toString());  
  return false;
  
 }
 //@Override
 public void onLongPress(MotionEvent e)
 { 
  Log.d("---onLongPress---",e.toString()); 
  
 }
 //@Override
 public boolean onScroll(MotionEvent e1,
   MotionEvent e2, float distanceX,   float distanceY)
 { 
  Log.d("---onScroll---",e1.toString()+e2.toString()); 
  return false;
  
 }
 //@Override
 public void onShowPress(MotionEvent e)
 { 
  Log.d("---onShowPress---",e.toString());
  
 }
 //@Override
 public boolean onSingleTapUp(MotionEvent e)
 { 
  Log.d("---onSingleTapUp---",e.toString()); 
  return false;
  
 }
 //@Override
 public boolean onDoubleTap(MotionEvent e)
 { 
  Log.d("---onDoubleTap---",e.toString()); 
  return false;
  
 }
 //@Override
 public boolean onDoubleTapEvent(MotionEvent e)
 { 
  Log.d("---onDoubleTapEvent---",e.toString());  
  return false;
  
 }
 //@Override
 public boolean onSingleTapConfirmed(MotionEvent e)
 { 
  Log.d("---onSingleTapConfirmed---",e.toString()); 
  return false;
  
 }

 
}

 
if we want to understand all gesture types and how they work, first of all we need to know three basic MotionEvents which can combine with each other and create some gestures, these three Events are Action_Down , Action_Move and Action_Up , each time you touch the screen an Action_Down occurs and when you start moving it will create Action_Move event and finally when you take your finger off the screen an Action_Up Event will be created.
onDown() is called simply when there is an Action_Down event.
onShowPress() is called after onDown() and before any other callback method, I found out it sometimes might not get called for example when you tap on the screen so fast, but it's actually what this method all about, to make a distinction between a possible unintentional touch and an intentional one.
onSingleTapUp() is called when there is a Tap Gesture. Tap Gesture happens when an Action_Down event followed by an Action_Up event(like a Single-Click).
onDoubleTap() is called when there is two consecutive Tap gesture(like a Double-Click).
onSingleTapConfirmed() is so similar to onSingleTapUp() but it is only get called when the detected tap gesture is definitely a single Tap and not part of a double tap gesture.
Here is the sequence in which these callback methods are called when we tap on the screen:

onDown() – onShowPress() - onSingleTapUp() – onSingleTapConfirmed()


and here is when we do a double tap:


onDown() – onShowPress() - onSingleTapUp() – onDoubleTap() – onDoubleTapEvent()
onDown() – onShowPress() – onDoubleTapEvent()


onFling() is called when a Fling Gesture is detected. fling Gesture occurs when there is an Action_Down then some Action_Move events and finally an Action_Up, but they must take place with a specified movement and velocity pattern to be considered as a fling gesture. for example if you put your finger on the screen and start moving it slowly and then remove your finger gently it won’t be count as a fling gesture.

onScroll() is usually called when there is a Action_Move event so if you put your finger on the screen and move it for a few seconds there will be a method call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... - onScroll()


or If the movement was a Fling Gesture, then there would be a call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... – onFling()

If there is an Action_Move event between first tap and second tap of a doubleTap gesture it will be handled by calling onDoubleTapEvent() instead of onScroll() method. onDoubleTapEvent() receives all Action_Up events of a doubleTap gesture as well.

Remember that if we touch the screen and don’t remove our finger for a specified amount of time onLongPress() method is called and in most cases there will be no further event callback regardless of whatever we do after that, moving or removing our finger. we can easily change this behavior of detector by calling setIsLongpressEnabled() method of GestureDetector class.

although GestureDetector makes our life much easier, it still could be a real pain in the ass if we would need to handle some complicated gestures, imagine you need an application which should do task1 when there is a circle gesture, task2 for rectangle gesture and task3 for triangle gesture. obviously it would not be so pleasant to deal with such a situation with those mechanism we have seen so far, it's actually when Gesture API and Gesture Builder Application come into play.

Gesture Builder Application comes with Android emulator as a pre-installed app, It helps us to simply make new gestures and save them on the device, then we can retrieve and detect those gestures later using Gesture API.
 I'm not gonna talk about Gesture Builder app here, 제스처 빌더를 여기서 말하려 하는거 아니다.
since there is a pretty good Article about it on Android Developers blog.
 
// 제스처 오버레이 뷰 클래스
the only thing I'd like to mention here is GestureOverlayView class,
 It is actually just a transparent FrameLayout which can detect gestures but the thing is it can be used in two completely different ways, you can either put other views inside it and use it as a parent view or put it is the last child view of a FrameLayout (or any other way which causes it to be placed on top of another view).
 
//첫번째 시나리오  : 모든 차일드 뷰가 터치 이벤트를 받는다..
In the first Scenario all child views will receive Touch events,
 therefore we will be able to press buttons or interact with other widgets as well as doing some gestures, on the other hand if GestureOverlayView has been placed on top, it will swallow all Touch events and no underlay view will be notified for any touch Event.

Although Gesture API brings many useful features for us,
I personally prefer to use GestureDetector for some simple,
basic gestures and honestly I feel like something is missing here,
I mean , apart from games, I would say more than 70% of all gestures
that might be needed in our applications are just a simple sliding in different directions or a double tap.
and that's why I have decided to come up with something easy
which can enable us to handle those 70% as simple as possible. how simple? you might be asking...

here is how our previous activity will look like if we use SimpleGestureFilter class :

 public class SimpleActivity extends Activity implements SimpleGestureListener{

private SimpleGestureFilter detector;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        detector = new SimpleGestureFilter(this,this);
    }
  
  @Override
public boolean dispatchTouchEvent(MotionEvent me){
   this.detector.onTouchEvent(me);
  return super.dispatchTouchEvent(me);
}

  @Override
public void onSwipe(int direction) {
  String str = "";
 
  switch (direction) {
 
  case SimpleGestureFilter.SWIPE_RIGHT : str = "Swipe Right";
                                           break;
  case SimpleGestureFilter.SWIPE_LEFT :  str = "Swipe Left";
                                                 break;
  case SimpleGestureFilter.SWIPE_DOWN :  str = "Swipe Down";
                                                 break;
  case SimpleGestureFilter.SWIPE_UP :    str = "Swipe Up";
                                                 break;
                                          
  }
   Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}

@Override
public void onDoubleTap() {
    Toast.makeText(this, "Double Tap", Toast.LENGTH_SHORT).show();
}

}

 

and here is SimpleGestureFilter source code :

 public class SimpleGestureFilter extends SimpleOnGestureListener{
   
public final static int SWIPE_UP    = 1;
public final static int SWIPE_DOWN  = 2;
public final static int SWIPE_LEFT  = 3;
public final static int SWIPE_RIGHT = 4;

public final static int MODE_TRANSPARENT = 0;
public final static int MODE_SOLID       = 1;
public final static int MODE_DYNAMIC     = 2;

private final static int ACTION_FAKE = -13; //just an unlikely number
private int swipe_Min_Distance = 100;
private int swipe_Max_Distance = 350;
private int swipe_Min_Velocity = 100;

private int mode      = MODE_DYNAMIC;
private boolean running = true;
private boolean tapIndicator = false;

private Activity context;
private GestureDetector detector;
private SimpleGestureListener listener;


public SimpleGestureFilter(Activity context,SimpleGestureListener sgl) {

  this.context = context;
  this.detector = new GestureDetector(context, this);
  this.listener = sgl;
}

public void onTouchEvent(MotionEvent event){
 
   if(!this.running)
  return; 
 
   boolean result = this.detector.onTouchEvent(event);
 
   if(this.mode == MODE_SOLID)
    event.setAction(MotionEvent.ACTION_CANCEL);
   else if (this.mode == MODE_DYNAMIC) {
 
     if(event.getAction() == ACTION_FAKE)
       event.setAction(MotionEvent.ACTION_UP);
     else if (result)
       event.setAction(MotionEvent.ACTION_CANCEL);
     else if(this.tapIndicator){
      event.setAction(MotionEvent.ACTION_DOWN);
      this.tapIndicator = false;
     }
 
   }
   //else just do nothing, it's Transparent
}

public void setMode(int m){
  this.mode = m;
}

public int getMode(){
  return this.mode;
}

public void setEnabled(boolean status){
  this.running = status;
}

public void setSwipeMaxDistance(int distance){
  this.swipe_Max_Distance = distance;
}

public void setSwipeMinDistance(int distance){
  this.swipe_Min_Distance = distance;
}

public void setSwipeMinVelocity(int distance){
  this.swipe_Min_Velocity = distance;
}

public int getSwipeMaxDistance(){
  return this.swipe_Max_Distance;
}

public int getSwipeMinDistance(){
  return this.swipe_Min_Distance;
}

public int getSwipeMinVelocity(){
  return this.swipe_Min_Velocity;
}


@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
   float velocityY) {

  final float xDistance = Math.abs(e1.getX() - e2.getX());
  final float yDistance = Math.abs(e1.getY() - e2.getY());

  if(xDistance > this.swipe_Max_Distance || yDistance > this.swipe_Max_Distance)
   return false;

  velocityX = Math.abs(velocityX);
  velocityY = Math.abs(velocityY);
        boolean result = false;

if(velocityX > this.swipe_Min_Velocity && xDistance > this.swipe_Min_Distance){
   if(e1.getX() > e2.getX()) // right to left
    this.listener.onSwipe(SWIPE_LEFT);
   else
    this.listener.onSwipe(SWIPE_RIGHT);
  
   result = true;
  }
  else if(velocityY > this.swipe_Min_Velocity && yDistance > this.swipe_Min_Distance){
   if(e1.getY() > e2.getY()) // bottom to up
    this.listener.onSwipe(SWIPE_UP);
   else
    this.listener.onSwipe(SWIPE_DOWN);
  
   result = true;
  }

   return result;
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
  this.tapIndicator = true;
  return false;
}

@Override
public boolean onDoubleTap(MotionEvent arg0) {
  this.listener.onDoubleTap();;
  return true;
}

@Override
public boolean onDoubleTapEvent(MotionEvent arg0) {
  return true;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent arg0) {
 
  if(this.mode == MODE_DYNAMIC){        // we owe an ACTION_UP, so we fake an      
     arg0.setAction(ACTION_FAKE);      //action which will be converted to an ACTION_UP later.                                   
     this.context.dispatchTouchEvent(arg0); 
  }  
    
  return false;
}


    static interface SimpleGestureListener{
     void onSwipe(int direction);
     void onDoubleTap();
}

}

 

as you can see clients of these class can determine the minimum and maximum distance and also minimum velocity which is required for a movement on screen to be considered as a Swipe Gesture,

 

I also thought it would be great if our filter can behave differently like what GestureOverlayView

can do and even more than that!


this Filter can run in three different mode:

Transparent, Solid and Dynamic.

 

in Transparent mode

it will work just like when we have a GestureOverlayView as parent: all views will receive Touch events;

Solid mode works

like when we put a GestureOverlayView as a child view:

no one will receive TouchEvent, it is not as efficient as GestureOverlayView is,

since we actually let all events get passed but what we do is we literally kill them

when they are passing through our filter ;).


the last mode is Dynamic mode,

the primary purpose of this mode is to have a bit smarter gesture detection, i mean there has been sometimes that i wanted to slide from one page to another, but a button get pressed and something else happens.

 

it does not happen so much but it is really annoying. what i tried to do in Dynamic mode is to distinguish between a swipe/double tap gesture and a movement which is neither of them.

 

so if you have a view full of buttons and other interactive stuff and user does a swipe or double tap gesture, it is guaranteed (although i believe there is no such thing as guarantee in life ;) )

that no other event callback will be called but only onSwipe() or onDoubleTap().


Anyway that's what i came up with to take the pain away in those circumstances

when we just need to handle some simple Gestures.


hope it will be helpful for you and can make your life a bit easier.

 

http://android-journey.blogspot.com/2010/01/android-gestures.html

 

 

Gesture Builder APK

http://code.google.com/p/quickdroid/downloads/detail?name=com.android.gesture.builder.apk&can=2&q=

 

Source

 /*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.android.gesture.builder

import _root_.android.app.{Dialog, AlertDialog, ListActivity}
import _root_.android.app.Activity._
import _root_.android.os.{AsyncTask, Bundle, Environment}
import _root_.android.view.{ContextMenu, View, MenuItem, LayoutInflater, ViewGroup}
import _root_.android.gesture.{Gesture, GestureLibrary, GestureLibraries}
import _root_.android.widget.{AdapterView, ArrayAdapter, EditText, TextView, Toast}
import _root_.android.content.{Context, DialogInterface, Intent}
import _root_.android.content.res.Resources
import _root_.android.text.TextUtils
import _root_.android.graphics.Bitmap
import _root_.android.graphics.drawable.{BitmapDrawable, Drawable}

import java.util.Comparator
import java.io.File

import scala.collection.JavaConversions.{JListWrapper, JSetWrapper}
import scala.collection.mutable.{HashMap, SynchronizedMap}

object GestureBuilderActivity {
 
private final val STATUS_SUCCESS = 0
 
private final val STATUS_CANCELLED = 1
 
private final val STATUS_NO_STORAGE = 2
 
private final val STATUS_NOT_LOADED = 3

 
private final val MENU_ID_RENAME = 1
 
private final val MENU_ID_REMOVE = 2

 
private final val DIALOG_RENAME_GESTURE = 1

 
private final val REQUEST_NEW_GESTURE = 1
   
 
// Type: long (id)
 
private final val GESTURES_INFO_ID = "gestures.info_id"

 
private final val mStoreFile =
   
new File(Environment.getExternalStorageDirectory, "gestures")
 
private final val sStore = GestureLibraries.fromFile(mStoreFile)

 
def getStore: GestureLibrary = sStore
 
 
class NamedGesture(var name: String, var gesture: Gesture)
}

class GestureBuilderActivity extends ListActivity {
 
import GestureBuilderActivity._  // companion object

 
private final val mSorter = new Comparator[NamedGesture]() {
   
def compare(object1: NamedGesture, object2: NamedGesture): Int = {
      object1
.name compareTo object2.name
   
}
 
}

 
private var mAdapter: GesturesAdapter = _
 
private var mTask: GesturesLoadTask = _
 
private var mEmpty: TextView = _

 
private var mRenameDialog: Dialog = _
 
private var mInput: EditText = _
 
private var mCurrentRenameGesture: NamedGesture = _

 
override protected def onCreate(savedInstanceState: Bundle) {
   
super.onCreate(savedInstanceState)

    setContentView
(R.layout.gestures_list)

    mAdapter
= new GesturesAdapter(this)
    setListAdapter
(mAdapter)

    mEmpty
= findViewById(android.R.id.empty).asInstanceOf[TextView]
    loadGestures
()

    registerForContextMenu
(getListView)
 
}

 
//@SuppressWarnings({"UnusedDeclaration"})
 
def reloadGestures(v: View) {
    loadGestures
()
 
}

 
//@SuppressWarnings({"UnusedDeclaration"})
 
def addGesture(v: View) {
    val intent
= new Intent(this, classOf[CreateGestureActivity])
    startActivityForResult
(intent, REQUEST_NEW_GESTURE)
 
}

 
override protected def onActivityResult(requestCode: Int,
                                          resultCode
: Int, data: Intent) {
   
super.onActivityResult(requestCode, resultCode, data)
       
   
if (resultCode == RESULT_OK) {
      requestCode match
{
       
case REQUEST_NEW_GESTURE =>
          loadGestures
()
     
}
   
}
 
}

 
private def loadGestures() {
   
if (mTask != null && mTask.getStatus != AsyncTask.Status.FINISHED) {
      mTask cancel
true
   
}        
    mTask
= new GesturesLoadTask().execute().asInstanceOf[GesturesLoadTask]
 
}

 
override protected def onDestroy() {
   
super.onDestroy()

   
if (mTask != null && mTask.getStatus != AsyncTask.Status.FINISHED) {
      mTask
.cancel(true)
      mTask
= null
   
}

    cleanupRenameDialog
()
 
}

 
private def checkForEmpty() {
   
if (mAdapter.getCount == 0) {
      mEmpty setText R
.string.gestures_empty
   
}
 
}

 
override protected def onSaveInstanceState(outState: Bundle) {
   
super.onSaveInstanceState(outState)

   
if (mCurrentRenameGesture != null) {
      outState
.putLong(GESTURES_INFO_ID, mCurrentRenameGesture.gesture.getID)
   
}
 
}

 
override protected def onRestoreInstanceState(state: Bundle) {
   
super.onRestoreInstanceState(state)

    val id
= state.getLong(GESTURES_INFO_ID, -1)
   
if (id != -1) {
      val entries
= new JSetWrapper(sStore.getGestureEntries)
     
var found = false
     
for (name <- entries if !found) {
        val gestures
= new JListWrapper(sStore.getGestures(name))
       
for (gesture <- gestures if !found) {
         
if (gesture.getID == id) {
            mCurrentRenameGesture
= new NamedGesture(name, gesture)
            found
= true
         
}
       
}
     
}
   
}
 
}

 
override def onCreateContextMenu(menu: ContextMenu, v: View,
                                   menuInfo
: ContextMenu.ContextMenuInfo) {
   
super.onCreateContextMenu(menu, v, menuInfo)

    val info
= menuInfo.asInstanceOf[AdapterView.AdapterContextMenuInfo]
    menu setHeaderTitle info
.targetView.asInstanceOf[TextView].getText

    menu
.add(0, MENU_ID_RENAME, 0, R.string.gestures_rename)
    menu
.add(0, MENU_ID_REMOVE, 0, R.string.gestures_delete)
 
}

 
override def onContextItemSelected(item: MenuItem): Boolean = {
    val menuInfo
=
      item
.getMenuInfo.asInstanceOf[AdapterView.AdapterContextMenuInfo]
    val gesture
= menuInfo.targetView.getTag.asInstanceOf[NamedGesture]

    item
.getItemId match {
     
case MENU_ID_RENAME =>
        renameGesture
(gesture)
       
true
     
case MENU_ID_REMOVE =>
        deleteGesture
(gesture)
       
true
     
case _ =>
       
super.onContextItemSelected(item)
   
}
 
}

 
private def renameGesture(gesture: NamedGesture) {
    mCurrentRenameGesture
= gesture
    showDialog
(DIALOG_RENAME_GESTURE)
 
}

 
override protected def onCreateDialog(id: Int): Dialog = {
   
if (id == DIALOG_RENAME_GESTURE)
      createRenameDialog
()
   
else
     
super.onCreateDialog(id)
 
}

 
override protected def onPrepareDialog(id: Int, dialog: Dialog) {
   
super.onPrepareDialog(id, dialog)
   
if (id == DIALOG_RENAME_GESTURE) {
      mInput setText mCurrentRenameGesture
.name
   
}
 
}

 
private def createRenameDialog(): Dialog = {
    val layout
= View.inflate(this, R.layout.dialog_rename, null)
    mInput
= layout.findViewById(R.id.name).asInstanceOf[EditText]
    val tv
= layout.findViewById(R.id.label).asInstanceOf[TextView]
    tv setText R
.string.gestures_rename_label

    val builder
= new AlertDialog.Builder(this)
    builder setIcon
0
    builder setTitle getString
(R.string.gestures_rename_title)
    builder setCancelable
true
    builder setOnCancelListener
new /*Dialog*/DialogInterface.OnCancelListener {
     
def onCancel(dialog: DialogInterface) {
        cleanupRenameDialog
()
     
}
   
}
    builder
.setNegativeButton(getString(R.string.cancel_action),
     
new /*Dialog*/DialogInterface.OnClickListener {
       
def onClick(dialog: DialogInterface, which: Int) {
          cleanupRenameDialog
()
       
}
     
}
   
)
    builder
.setPositiveButton(getString(R.string.rename_action),
     
new /*Dialog*/DialogInterface.OnClickListener() {
       
def onClick(dialog: DialogInterface, which: Int) {
          changeGestureName
()
       
}
     
}
   
)
    builder setView layout
    builder
.create()
 
}

 
private def changeGestureName() {
    val name
= mInput.getText.toString
   
if (!TextUtils.isEmpty(name)) {
      val renameGesture
= mCurrentRenameGesture
      val adapter
= mAdapter
      val count
= adapter.getCount

     
// Simple linear search, there should not be enough items to warrant
     
// a more sophisticated search
     
var found = false
     
for (i <- 0 until count if !found) {
        val gesture
= adapter.getItem(i)
       
if (gesture.gesture.getID == renameGesture.gesture.getID) {
          sStore
.removeGesture(gesture.name, gesture.gesture)
          gesture
.name = mInput.getText.toString
          sStore
.addGesture(gesture.name, gesture.gesture)
          found
= true
       
}
     
}

      adapter
.notifyDataSetChanged()
   
}
    mCurrentRenameGesture
= null
 
}

 
private def cleanupRenameDialog() {
   
if (mRenameDialog != null) {
      mRenameDialog
.dismiss()
      mRenameDialog
= null
   
}
    mCurrentRenameGesture
= null
 
}

 
private def deleteGesture(gesture: NamedGesture) {
    sStore
.removeGesture(gesture.name, gesture.gesture)
    sStore
.save()

    val adapter
= mAdapter
    adapter setNotifyOnChange
false
    adapter remove gesture
    adapter sort mSorter
    checkForEmpty
()
    adapter
.notifyDataSetChanged()

   
Toast.makeText(this, R.string.gestures_delete_success,
                   
Toast.LENGTH_SHORT).show()
 
}

 
private class GesturesLoadTask extends AsyncTask[AnyRef, NamedGesture, Int] {
   
private var mThumbnailSize: Int = _
   
private var mThumbnailInset: Int = _
   
private var mPathColor: Int = _

   
override protected def onPreExecute() {
     
super.onPreExecute()

      val resources
= getResources
      mPathColor
= resources getColor R.color.gesture_color
      mThumbnailInset
=
        resources
.getDimension(R.dimen.gesture_thumbnail_inset).toInt
      mThumbnailSize
=
        resources
.getDimension(R.dimen.gesture_thumbnail_size).toInt

      findViewById
(R.id.addButton) setEnabled false
      findViewById
(R.id.reloadButton) setEnabled false
           
      mAdapter setNotifyOnChange
false          
      mAdapter
.clear()
   
}

   
// temporary workaround (compiler bug !?)
   
private def publishProgress1(values: NamedGesture*) {
     
super.publishProgress(values: _*)
   
}

   
override protected def doInBackground(params: AnyRef*): Int = {
     
if (isCancelled())
        STATUS_CANCELLED
     
else if (! (Environment.MEDIA_MOUNTED equals
                 
Environment.getExternalStorageState))
        STATUS_NO_STORAGE
     
else if (sStore.load()) {
        val entries
= new JSetWrapper(sStore.getGestureEntries)
       
for (name <- entries if !isCancelled()) {
          val gestures
= new JListWrapper(sStore.getGestures(name))
         
for (gesture <- gestures) {
            val bitmap
= gesture.toBitmap(mThumbnailSize, mThumbnailSize,
                                          mThumbnailInset
, mPathColor)
            val namedGesture
= new NamedGesture(name, gesture)

            mAdapter
.addBitmap(namedGesture.gesture.getID, bitmap)
            publishProgress1
(namedGesture)
         
}
       
}

        STATUS_SUCCESS
     
} else
        STATUS_NOT_LOADED
   
}

   
override protected def onProgressUpdate(values: NamedGesture*) {
     
super.onProgressUpdate(values: _*)

      val adapter
= mAdapter
      adapter setNotifyOnChange
false

     
for (gesture <- values) {
        adapter add gesture
     
}

      adapter sort mSorter
     
      adapter
.notifyDataSetChanged()
   
}

   
override protected def onPostExecute(result: Int) {
     
super.onPostExecute(result)

     
if (result == STATUS_NO_STORAGE) {
        getListView setVisibility
View.GONE
        mEmpty setVisibility
View.VISIBLE
        mEmpty setText getString
(R.string.gestures_error_loading,
                                 mStoreFile
.getAbsolutePath)
     
} else {
        findViewById
(R.id.addButton) setEnabled true
        findViewById
(R.id.reloadButton) setEnabled true
        checkForEmpty
()
     
}
   
}
 
}

 
private class GesturesAdapter(context: Context)
 
extends ArrayAdapter[NamedGesture](context, 0) {
   
private val mInflater: LayoutInflater =
      context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
   
private val mThumbnails =
     
new HashMap[Long, Drawable] with SynchronizedMap[Long, Drawable]

   
def addBitmap(id: Long, bitmap: Bitmap) {
      mThumbnails
(id) = new BitmapDrawable(bitmap)
   
}

   
override def getView(position: Int, convertView: View,
                         parent
: ViewGroup): View = {
      val convertView1
= if (convertView == null)
        mInflater
.inflate(R.layout.gestures_item, parent, false)
     
else
        convertView

      val gesture
= getItem(position)
      val label
= convertView1.asInstanceOf[TextView]

      label setTag gesture
      label setText gesture
.name
      label
.setCompoundDrawablesWithIntrinsicBounds(
         mThumbnails
(gesture.gesture.getID), null, null, null)

      convertView1
   
}
 
}
}

http://www.assembla.com/code/scala-eclipse-toolchain/git/nodes/docs/android-examples/GestureBuilder/src/com/android/gesture/builder/GestureBuilderActivity.scala?rev=334c117e3292c12e728639de6c19056b3513d877 

 // 속성창에 설정된 값--------------android:background="@drawable/backpattern"
        LinearLayout linear =(LinearLayout)findViewById(R.id.main_linear);
        linear.setBackgroundResource(R.drawable.backpattern);

 

안드로이드 폰의 슬라이드가 열리고(landscape),  닫혀서(portrait) 화면이 전환될 때 
필요한 처리를 삽입하는 부분입니다.

아래 코드와 같이 액티비티 코드 내에 onConfigurationChanged() 함수를 오버라이딩하여 사용하면 됩니다.

 
  @Override
  public void onConfigurationChanged(Configuration newConfig) { 
     super.onConfigurationChanged(newConfig);

      Log.i(LOGTAG, "=== onConfigurationChanged is called !!! ===");
           
   if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { // 슬라이드 닫힐때
    Log.i(LOGTAG, "=== Configuration.ORIENTATION_PORTRAIT !!! ===");   
   } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { // 슬라이드 열릴때
    Log.i(LOGTAG, "=== Configuration.ORIENTATION_LANDSCAPE !!! ===");   
   }
  }

 

 

안드로이드폰은 화면 회전이 지원된다.

키보드를 열거나 닫으면 가로보기/세로보기로 전환이 되는데,
이때 UI가 새로 그려지면서 Activity의 onDestroy()와 onCreate() 가 수행된다.

위 과정이 수행되고 나면,
Activity 에서 가지고 있었던 변수들(field 도 포함)이 초기 상태로 된다.

만약, 코드에서 Thread를 만들어 돌아가는 중이었다면,
화면 회전을 한 후에는 사라지는 현상이다.

/**
Activity소스코드를 보면, 타입이 HashMap<String, Object>이고, null 을 리턴하고 있다.
유지해야할 데이터가 한개라면 그 Object를 바로 리턴해도 된다. */
@Override
public Object onRetainNonConfigurationInstance() {
       HashMap<String, Object> map = new HashMap<String, Object>();
       map.put("worker", worker);
       map.put("var1", var1);
       map.put("var2", var2);
       return map;
}
}

/**
onCreate의 적당한 부분에 이전 데이터를 복원하는 코드를 넣어준다.
여기에서는 restore() 를 따로 정의했다. */
@Override
public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       ...
       ...
       restore();
       ...
       ...
}

/**
이전 데이터를 복원한다 */
@SuppressWarnings("unchecked")
private void restore() {
       Object o = getLastNonConfigurationInstance();
       if(o!=null){
              //Map형태로 리턴했기때문에 casting 해서 사용한다.
              HashMap<String, Object>map = (HashMap<String, Object>) o;
              this.worker = (Thread) map.get("worker");
              this.var1 = (String) map.get("var1");
              this.var2 = (String) map.get("var2");
       }
}

화면이 회전되거나 종료가 될때, onDestroy()가 호출되는데,
다음과 같이 구분해서 종료될때의 처리를 해 줄 수 있다.

@Override
protected void onDestroy() {
       Log.d(tag, "onDestroy" + " isFinishing : " +isFinishing());
       if(isFinishing()){
              //isFinishing()은 진짜로 프로그램이 종료될때는 true 값이다.
              // 회전할때는 당연히 false
              worker.interrupt();
              worker=null;
       }
       super.onDestroy();
}

 

Android 프로그래밍중 화면을 가로로 할지 세로로 할지를 정할수 있습니다.
우선 가상 머신을 실행 시키도록합니다.

위의 화면은 세로로 보이는 기본 화면 입니다.

이후 Ctrl + F11을 누르시면
가로 화면으로 전환됩니다.

아래의 화면은 가로로 설정된 화면의 모습입니다.

추가 적으로 한 가지 더 설명 드리자면

프로그램 개발시 강제로 가로로 혹은 세로로 고정해야 할경우가 있습니다.
(이것을 설정하지 않을경우 모바일의 위치에 따라 가로 혹은 세로로 모드가 변경될것입니다.)

고정 방법은
Project->AndroidManiFest.XML에서

        <activity android:name=".Memory"
                  android:label="@string/app_name">
Activity 부분에

android:screenOrientation="portrait"(세로 고정) 
혹은
android:screenOrientation="landscape"(가로 고정)
을 추가 하여 줍니다.

'0.일반개발' 카테고리의 다른 글

Android 2.0 변경사항  (0) 2010.09.14
안드로이드 제스쳐 Android Gesture  (0) 2010.09.14
EditText 입력 글자수 제한 / InputFilter  (0) 2010.09.13
플래시 어플리케이션 개발하기  (0) 2010.09.13
네이버 Flash 2010 04  (0) 2010.09.13

입력받는 문자갯수 정하기Filter 사용
다음은 filter를 사용하여 입력받는 문자열의 갯수를 정하는 방법이다. HTML input의 maxlength 속성과 같은 역할이다.

EditText mLimitEdit = (EditText) findViewById(R.id.edit);
mLimitEdit.setFilter(new InputFilter[] {
   new InputFilter.LengthFilter(3)
});
http://www.diebuster.com/?p=951

개발자라면 모두 자기 만의 프로젝트관리 방법이 있을 거라 생각합니다. 이번에 다룰 주제는 하나의 어플리케이션을 개발하기 위해 어떻게 준비하고 진행해야 하는가? 에 대한 내용입니다. 개인적인 경험을 정리한 것이므로 절대적인 기준 같은걸 설명 드리거나 일반론을 말씀 드리는 건 아닙니다. 단지 어떻게 하는 게 효율적이고 안정적인 어플리케이션 작성에 도움이 되었다 라는 저의 요령 같은 걸 공개한다고 생각해주시면 되겠습니다. 꼭 제 글이 아니라도 보다 쾌적하고 생산성 높은 작업을 위해 한번 쯤 프로젝트의 실질적인 프로세스를 생각해보시는 건 언제나 큰 도움이 될 듯합니다.

단지 실무적인 내용을 정리하는 글이다 보니 다른 글처럼 초보자에 대한 깊은 배려는 없을지도 모릅니다 ^^;

 

1단계. 설계단계 – AS3 격리 수준을 이용한 설계

 

프로젝트를 진행하기에 앞서 먼저 AS3에 대한 아주 기본적인 지식이 필요합니다. 아니 정확하게는 OOP 언어로 무언가를 설계 시에 기본적으로 고려해야 하는  지식일 수도 있습니다. 바로 격리단계에 대한 이야기입니다.

AS3는 언어적인 수준에서 3가지 격리단계를 지원합니다. 즉 ApplicationDomain(이하AD), namespace(이하 NS), package 라는 겁니다. 하나의 어플은 여러 개의 클래스로 구성하는데 이러한 클래스 설계 시에 반드시 저 세 가지의 카테고리를 모두 고려하여 분배하는 습관을 가져야 합니다.

언제나 처럼 게임으로 예를 들며 생각해보겠습니다.

ApplicationDomain 수준에 대해

  • stage2에서 사용될 배경과 적에 대한 처리를 담당하는 Cstage2 라는 클래스가 있다고 합시다.
  • 그럼 우선적으로 이 클래스는 Main AD소속이 아닐 가능성이 높습니다. Main.swf 에 소속된 클래스가 아니라 stage2.swf 에 소속된 클래스일 가능성이 높죠.
  • 따라서 이미 AD 상 본체와 분리시킬 생각을 한다면 Cstage2클래스 내부에서는 MainAD에 선언된 클래스나 여러 가지 참조를 사용할 생각을 하면 안됩니다. 바로 AD 수준의 격리 로직을 고려해야만 하는 상황이죠.
  • 하지만 게임이 운영되는 시점에 MainAD에 있는 것들과 전혀 객체통신을 하지 않는다는 것은 말이 안됩니다. 결국 이러한 문제는 MainAD, 공유AD, stage2AD 라는 3가지 영역이 이미 필요하다는 사실을 암시합니다.
  • AD를 구별짓는 격리화 수준은 클래스의 모든 로직에 관여하므로 나중에 아무렇게나 swf로 빼내서 로딩할 문제가 아닙니다. 만약 이러한 고려가 없이 설계된 클래스들이라면 그저 런타임 시엔 MainAD로 로딩되거나 MainAD를 상속받은 AD를 사용할 수 밖에 없고, 이는 GC도 안되고 덮어쓰기도 안된다는 얘기입니다.
  • 이와 관련되어 보다 자세한 AD의 얘기는 여기서 다루고 있습니다.

Namespace 수준에 대해

  • NS는 컴파일타임에 격리를 확인해주는 커스텀 격리 장치로 이해할 수 있습니다. 왜냐면 이는 반드시 코드 상의 구상을 하면 회피할 수 있는 것으로 사용 못하게 강제할 수는 없는 장치이기 때문입니다. 
  • 즉 NS가 public인 경우는 다른 클래스가 구상해버리면 그만이고 internal이나 private로 NS를 선언하면 이미 저 한정자 수준으로는 외부의 클래스가 접근 할 수 없으므로 구지 NS를 사용하지 않아도 되는 문제가 됩니다. 콜린옹의 책에서는 무려 NS를 동적으로 생성 할당하는 식으로 NS의 의미를 찾아 보실라 하는데 제가 보기엔 미친 삽질..=.=;(절대 여러분은 그런 짓 하시면 안됩니다..) 따라서 요점은 NS는 일종의 안내용 마커라는 겁니다. 정확하게는 개발자가 코딩하는 시점에서 주의를 환기시켜주는 효과가 있는 마커입니다.
  • 그럼 NS가 격리화 수준에서의 뭐냐구요? 어쩌면 템플릿메쏘드 패턴과 유사합니다. 개발시 코딩 스타일을 강제하는 효과가 있습니다. 이 로직을 사용하고 싶으면 반드시 이 NS를 사용하여 이 스타일로 정의해라 라는 코딩 시의 규격을 표현할 수 있습니다. 개발자가 다른 개발자를 강제하는 컴파일타임에 검사가 되는 코딩 규칙 같은 걸로 바라보는 편이 정확하겠죠. 물론 내부적으로는 여러 가지 역활을 수행하고 있습니다. 대표적으로 as1, as2와 호환하기 위해 같은 이름을 갖는 Math등의 시스템 클래스를 각 as버전에 따라 분리 사용하는 역활로 AS3라는 NS를 사용하는 등의 목적으로 사용됩니다. 하지만 실제 개발할 때는 글쎄요..거의 쓸모는 없는 셈입니다.

Package 수준에 대해

  • 패키지는 자바를 비롯한 파생언어에서 격리화의 기준이 되는 매우 강력한 추상화 도구입니다. 패키지는 물리적인 위치를 분리해내는데 사용되기도 하지만 그보다 더 중요한 기능은 다수의 클래스를 묶어 하나의 의미로 표현하는 매우 강력한 추상화 도구라는 점입니다.
  • 이러한 패키지를 사람들이 주로 시스템내장 패키지부터 공부하는 경향이 있어 크게 오해하는 경우가 많습니다. 즉 flash.display.* 소속의 클래스들은 display라는 패키지의 소속이긴 하지만 거의 모든 클래스가 노출되어있습니다. 이는 제가 방금 설명한 display패키지가 하나의 의미로 표현되는 예라고 볼 수 없죠.
  • 즉 Sprite는 Sprite고, Bitmap은 Bitmap이지 둘이 합쳐져서 Display라는 어떠한 의미를 생성하지 않는다는 뜻입니다. 대부분의 언어에서 시스템 패키지는 이러한 형태로 단지 의미론적으로 구별 짓는 카테고리 역할만 합니다. 그도 그럴게 시스템이 제공하는 클래스는 이미 클래스 수준에서 시스템의 기능을 대신하기 때문에 하나하나의 클래스가 더 이상 추상화하거나 묶어서 표현할 필요 없이 단일객체로 완전한 상태의 클래스인 경우가 대부분이기 때문입니다. 따라서 단지 그러한 완성된 의미체계로서의 클래스를 대략 묶어주기만 하면 되는거죠. 덕분에 DisplayObject의 상속과 전혀 무관하게 media라던가 text에 그 자식들이 들어가 있는걸 볼 수 있습니다. DisplayObject가 추상클래스인걸 생각해보면 참 이례적인 패키징이라 할 수 있습니다.
  • 하지만 여러분이 작성하실 클래스는 절대로 이러한 시스템패키지의 예를 따라 하시면 안됩니다. 패키지 단위의 설계를 반드시 실천해야 합니다. 패키지단위의 설계패키지 하나가 완전히 하나의 의미 체계를 가져야 한다 는 것으로 클래스 단일 책임의 확장판이라 생각하시면 됩니다. 즉 game.player 라는 패키지가 있다고 생각해봅시다. 이 패키지 안에는 CsinglePlayer, CnpcPlayer, CnetworkPlayer 등이 있을 수 있습니다. 하지만 이 패키지는 단일한 의미로 게임상에서 액션을 취하는 플레이어라는 거죠. 단지 그 상황이나 조건에 따라 어떤 상세 플레이어로직을 구현한 클래스가 선택될 것인가만 패키지내부에서 결정되는 식입니다. 따라서 이를 도와줄 클래스와 이 플레이어라는 의미 체게를 도와줄 클래스만 이 패키지 안에 존재해야 합니다.
    • CsinglePlayer, CnpcPlayer, CnetworkPlayer – 경우에 따라 로직을 따로 구현한 Player 서브 클래스
    • Iplayer – game.player 패키지의 의미 : 클래스의 의미는 외부에 인터페이스로 표현됩니다.
    • CplayerFactory – game.player패키지가 외부에 공개하는 생성용 API
  • 위의 예에서 결국 player서브클래스는 전부 internal 클래스이고, Iplayer와 CplayerFactory만 public이 되며 CplayerFactory.getPlayer()의 경우 반환을 Iplayer로 하게 됩니다. 즉 game.player패키지 자체가 완전히 player라는 단일 역할을 외부에는 인터페이스만 공개하고 패키지 내부에는 그를 도와줄 서브 클래싱으로 처리된거죠. 실제로 여러분의 솔루션에서도 반드시 패키지는 저러한 의미로서 사용되어야 합니다. 만물상패키지가 되어 그 안에 수많은 public클래스가 존재하는 것 자체가 패키지라는 격리체계를 전혀 사용하지 못하고 있다고 볼 수 있습니다.

이상의 기본적인 격리 수준을 완전히 이해하고 클래스 설계 시나 세부 알고리즘 구상 시에도 항상 염두 하여 패키지 넘어서의 참조에 조심하고 AD넘어서의 객체통신을 조심해야만 합니다. package에 추가적인 글은 여기에 있습니다.

 

 

2단계. 개발도구선택 – CS4와 빌더

 

저는 이 글을 읽는 분은 개발자라고 생각합니다. 개발자 분들은 모두 빌더만 사용하세요. CS4를 사용해야 하는 경우는 전혀 없습니다. 만약 있다면 그것은 디자인작업이 결부되어있는 경우이고, 그러한 경우 본인이 스스로 디자인이 결부된 부분만 따로 정리하여 cs4에서 swf를 생성하고 개발은 빌더에서 진행하여 그러한 swf를 에셋으로 처리하게 분리해주세요. 즉 cs4가 에셋을 생성하기 위해 부분적으로, 혹은 협업하는 디자이너가, 혹은 디자이너작업을 할 때의 자기 자신(분신?)이 사용하는 건 전혀 문제없습니다만, 개발이란 영역에서는 언제나 빌더를 사용하세요. 모든 cs4의 결과는 swf로 요약되고 그 swf는 반드시 빌더에서 embed나 loader로 연동할 수 있습니다. 왜라고 물어보시겠죠?

  • 이클립스 통합환경의 사용이 가능합니다. 수많은 개발을 위한 플러그인의 도움으로 IDE에서의 개발과는 비교가 되지 않을 정도로 안정적이고 생산성 높은 진행이 가능합니다.
  • 컴파일을 CS4로 하면 이클립스의 통합 개발 프로세스에서 많은 부분이 수동으로 할 수 밖에 없게 되고 이는 최종 생산물이 잘못될 가능성이 높아지게 됩니다. 따라서 컴파일까지도 완전히 빌더에서 처리해야합니다.
  • FLA파일은 악의 근원입니다. 이러한 통합파일은 항상 위험성을 내포합니다. 즉 라이브러리의 이미지가 바뀌어도 FLA를 열어서 업데이트하고 컴파일을 수동으로 하는 모든 과정이 위험합니다. 만약 빌더 환경이라면 공유폴더에 약속된 이름으로 그 이미지가 존재하고 디자이너가 그 이름만 지켜 네트웍드라이브로 카피해주고 개발자에게 알려주면 개발자는 어떠한 파일도 건드리지 않고 컴파일만 다시 하게 됩니다. 즉 어떠한 기존의 소스파일이나 프로젝트 관련파일도 건드리지 않는다는 것이죠.
  • 원격저장소 및 백업파일에 대한 완전한 지원이 가능해야만 복원, 복구 등이 자유롭습니다. CS4에서 개발한다는 건 내일을 보지 않고 오늘 지구가 멸망할거라 생각하는 것과 별 다를 바 없습니다(물론 디자인관련 작업 말고 개발 시에요 ^^;)
  • 수많은 다른 개발 툴이 있습니다. 강력히 권해드리는데 절대로 빌더를 사용하세요. FDT도 쓰지 마시고, 디벨로퍼도 쓰지 마시고, 기타 제가 잘 모르는 툴도 쓰지 마세요. 오피셜한 툴을 쓰시고 그 툴과 함께 어도비본사의 업그레이드 정책에 맞게 개발해 가시고 부족한 기능은 이클립스로 매꾸세요. 다른 툴이 무슨 묘기를 하던 별로 생산성과 상관없습니다. 그런 묘기가 많이 필요하다는 것 자체가 잘못된 설계 클래스를 개떡칠로 만들고 있다는 신호일 가능성이 농후합니다. 대부분의 리펙토링 책들은 메쏘드가 10줄이 넘어가면 경계하라고 하고 메쏘드의 수가 5개가 넘어가는 클래스를 경계하라고 가르칩니다.
  • 빌더의 설치는 절대로 플러그인으로 하세요. 현재 플래시빌더베타까지 해서 모든 빌더가 지원하는 이클립스 최대버전은 아쉽게도 3.4까지입니다. 가니메데를 설치하시고 플러그인으로 설치하세요. 그래야만 이클립스의 모든 플러그인을 제한없이 사용할 수 있기 때문입니다.
  • 이클립스와 플러그인의 설치에 대해서는 여기를 참고하세요.

 

 

3단계. 프로젝트의 셋팅

 

실무적인 프로젝트 셋팅은 길게 설명할 필요 없는 매우 실무적인 내용이라 간단간단히 항목으로 설명하겠습니다.

  1. SVN서버 섭외 – 일단 코드를 올릴 레포지토리를 확보합니다.
    • 반드시 하나의 프로젝트가 하나의 레포지토리를 확보하도록 섭외한다.(가장 좋게는 각자의 URI를 갖는 것이다)
    • /trunk/src/ 의 구조를 갖도록 생성한다.
  2. 이클립스에서 레포지토리 프로젝트를 시작하여 세팅한 후 위자드를 통해 as프로젝트나 flex lib 프로젝트로 설정한다.
  3. 다음과 같은 폴더 구조를 생성한다.
    • /lib/ – swc를 모아둔다.
    • /doc/ – asdoc의 결과를 저장한다.
    • /swf/ – 릴리즈 된 swf를 버전별로 백업한다.
    • /src/embed/ – 엠베딩 에셋을 모아둔다.
    • /src/remote/ – 원격지에서 런타임 로딩할 자원을 모아둔다.
  4. 최초 개발 단계의 작업 시엔 사내 라이브러리의 경우 swc보다는 패스에 라이브러리 폴더를 잡아준다. 프로젝트가 진행되면서 기존의 라이브러리가 기능이 추가되거나 버그가 수정되는 일은 매우 흔한 일이므로 이렇게 진행하는 편이 훨씬 효율적이다.
  5. 프로젝트가 완료된 후 반드시 현재 상태의 참조 라이브러리들의 그때 상태로 swc를 생성하여 lib에 카피한 후 패스를 전부 삭제하고 swc folder를 lib로 지정하여 컴파일이 문제없는지 확인한다.
  6. 레포지토리에 반드시 관련된 swc도 같이 넣어서 이후 라이브러리가 변경되어도 그 때 상태 그대로 언제나 컴파일이 가능하게 보존한다.
  7. remote에 저장된 런타임로딩용 자원들은 실서버로 대체될 때 코드상에서도 일괄 반영할 수 있도록 각 로더의 경로를 처음부터 ROOT_PATH+’/image/menu1.jpg’ 라는 식으로 생성하여 전역변수 한 개를 수정하여 처리할 수 있게 작성한다. 이러한 방식을 통해 개발 시에는 로컬에서 런타임로딩 테스트를 진행하고 완료 후엔 서버 상에서 자연스럽게 테스트를 진행할 수 있다.

또한 위의 프로젝트는 하나의 어플에 대해 하나만 생성하는 것이 아닙니다. 위에서 설명한 AD격리단계에 따라 격리된 AD수만큼 생성하는 것이 바람직합니다. 개발 시부터 근본적으로 AD넘어 호출이 불가능하도록 완전히 격리된 상태로 개발을 하는 것이죠. 하지만 프로젝트에 따라서는 어플리케이션 클래스를 여러 개 만들어서 관리하는 방법도 있습니다. 각 프로젝트의 특성에 맞게 선택하면 될 문제긴 하지만 실제 진행해보면 하나의 프로젝트 안에 여러 개의 어플리케이션클래스가 존재하는 경우 디플로이 위치문제나 AD건너 호출이 쉽다는 요인들이 항상 안좋게 작용한다는 게 일반적인 제 경험담입니다.

 

 

4단계. 컴파일 및 디버깅하기

 

일단 여러분들의 코딩스타일이나 진행에 대해서는 제가 무슨 의견을 드릴 필요는 없다고 생각하기 때문에 곧장 컴파일 및 디버깅 단계로 점프해보겠습니다. 최종적으로 코딩이 끝나고 컴파일 단계의 흐름은 다음과 같은 단계로 진행 하는 걸 추천해 드립니다.

  1. 코딩이 끝나면 디버그를 통해 로컬에서 디버그 버전 swf를 컴파일하여 디버깅 모드로 테스트를 진행한다.
  2. 1번의 안정성이 확인되면 이를 디플로이서버에 디버그 버전인 상태로 업로드하여 디버거와 연결하여 테스트한다.
  3. 2번의 안정성이 확인되면 로컬에서 릴리즈버전을 컴파일하고 이를 디플로이한다.
  4. 3번의 안정성이 확인되면 디플로이한 swf를 날짜이름과 버전을 붙여 위에 설명한 swf폴더에 백업해둔다.
  5. 이후 변경 사항이 생기면 1~4번까지의 과정을 다시 진행한다.

일단 원격지의 swf를 빌더에 연결하여 디버깅 하는 방법은 여기에 적어 두었습니다. 간단합니다 ^^; 원격디버깅을 제대로 하지 않아 로컬에서 잘 돌던게 디플로이 후 어딘가 에러가 생기고 어디가 에러인지도 로그로 파악하기 힘든 경우가 많습니다. 하지만 원격디버깅을 도입하면 브레이크포인트까지 완전히 먹기 때문에 완전한 서비스 환경에서도 디버깅을 진행할 수 있습니다. 대략 넘어가지 마시고 반드시 원격디버깅도 진행하시는 습관을 갖도록 하세요.

디버그버전의 컴파일과 릴리즈버전은 일단 용량에서만 70%정도로 릴리즈버전이 작아집니다. 성능이나 속도 또한 눈에 확 띌 정도로 릴리즈버전이 우수하므로 절대로 디버그 버전으로 서비스되게 하지 마세요.

 

 

5단계. 디플로이 자동화

 

거창한 건 아니라도 디플로이를 자동화하지 않는 건 항상 위험을 내포하고 있습니다(언제나 사람이 깜빡하는게 문제죠) 따라서 ANT라도 한 줄 짜두면 알 FTP로 사람이 손으로 일하는 위험을 최소화 시킬 수 있습니다. 근데! 제 블로그에 이것과 관련된 블로깅을 한적이 없어서 링크를 걸어드릴 게 없네요 ^^; 포스트 단일 책임의 원칙에 따라 이 글에 앤트 ftp사용법을 기술할 수 없기 때문에 지인의 포스트를 여기에 링크 겁니다. 참조하시라고 하고 싶으나 대략 셋팅만 설명하고 태스크 샘플을 안올려두셨네요. 나중에 궁금하시면 물어보시라는.. ant던 marvin이든 하다못해 deploy.bat 파일이라도 만드세요. 사람이 매번 손으로 그런 짓을 하게 두면 안됩니다. 사소한 조작오류가 큰 위험을 야기 합니다 ^^;

+ Recent posts