‏הצגת רשומות עם תוויות java. הצג את כל הרשומות
‏הצגת רשומות עם תוויות java. הצג את כל הרשומות

יום ראשון, 26 ביולי 2015

Java Native Interface


רוב האפליקציות ב Android משתמשות בספריות Java תחת ה JVM (ליתר דיוק Dalvik) ללא צרכים מיוחדים אבל בחלק מהמקרים זה לא מספיק ונדרשת התערבות עמוקה יותר, בדומה לאחד המאמרים בו הצגתי את JavaScript Interface גם פה מדובר בממשק שהמערכת נותנת שמאפשר לנו להתחבר לעולם ה Native במכשיר ולהפעיל דרכו ספריות בעזרת שפת C.

שימו לב!

  • המאמר נכתב על Android Studio 1.0.1
  • מערכת הפעלה Windows 7 64 Bit


אם לרגע חשבתם שמעכשיו תכתבו אפליקציות ב C אז כדאי שתעצרו (למרות שזה אפשרי אבל לא חבל..), לא בשביל זה יש את ה NDK אלא עבור מקרים מאוד מיוחדים כמו למשל מוגבלויות ה API של Java או יצירת ממשק עבור חומרה חדשה, יש שימוש רחב ב NDK בתעשיית המשחקים בגלל ממשק ל OpenGL רחב יותר,

דרישות
  • Native Development Kit - NDK
  • +Java Development Kit - JDK 1.8
  • +Gradle 2.2

יש להוריד ולפרוס את ה NDK היכן שתרצו (מומלץ לשמור איפה שתיקיית ה SDK), כדאי מאוד לוודא שיש לכם את גרסת Java אחרונה, גרסאות ישנות מידי עלולות לגרום לבעיות שלא נעים להתעסק בהן, לאחר מכן יש להוסיף את הנתיבים לתיקיית ה Java וה NDK למשתנה ה Path הכללי של המערכת שניתן להגיע אליו דרך Environment Variables שנמצא ב Advanced במסך של ה System Properties.

ניצור פרויקט חדש, נגדיר פונקצית Native שאת המימוש שלה נעשה ב C:

public native String getStringFromNative();


לאחר מכן נפתח Terminal ב Android Studio ,צריך לוודא שאתם בתיקיית Main של הפרויקט ולהריץ את הפקודה הבא:

E:\NDKtest3\app\src\main>javah -d jni -classpath E:\Users\proxytype\AppData\Local\Android\sdk\platforms
\android-14\android.jar;..\..\build\intermediates\classes\debug legend.proxytype.ndktest3.MainActivity

הפקודה תיצור תיקייה בפרויקט בשם JNI בתוכה קובץ Header שמכיל את החתימה לפונקציה, ניצור בה 2 קבצים חדשים, הראשון נקרא Application.mk שמכיל את הקוד הבא:

APP_ABI := all
APP_PLATFORM := android-14

השורה הראשונה מגדירה לאיזו ארכיטקטורת מעבד תתואם הספריה החיצונית (ה NDK מגיע עם כל סוגי ה Tool Chain) במקרה של ALL תיווצר ספריה עבור כל ארכטיקטורה, השורה השנייה עבור סוג הפלטפורמה של ה Android.

הקובץ השני נקרא Android.mk שמכיל את ההגדרות של הספריה בדומה ל Makefile סטנדרטי:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := MyLib
LOCAL_SRC_FILES := main.c
include $(BUILD_SHARED_LIBRARY)  

חשוב לזכור להמשך את 2 הפרמטרים הבאים, הראשון ה LOCAL_MODULE שהוא שם הספריה שנשתמש בו ב Java כפי שנראה בהמשך, ו LOCAL_SRC_FILES שמצביע לקובץ ה main.c שמכיל את המימוש לפונקציות בקובץ ה Header שנוצר בתחילת התהליך.

C

התוכנית תכיל פונקציה שתחזיר מחרוזת מספריית ה Native שנכתבה ב C לאפליקציה עצמה שנכתבה ב Java, יש ליצור קובץ main.c ולרשום בו את הקוד הבא:

 //our generated header file
#include "legend_proxytype_ndktest3_MainActivity.h"

JNIEXPORT jstring JNICALL 
Java_legend_proxytype_ndktest3_MainActivity_getStringFromNative(JNIEnv * env, jobject obj)
 {
        return (*env)->NewStringUTF(env,"Hello From JNI!");
 }
הקוד מאוד פשוט וניתן לראות את החשיפה של הפונקציה עבור מנגנון ה JNI והאפשרות לקרוא לה מבחוץ, קיים באג ב Android Studio שמחייב ליצור קובץ נוסף ריק על מנת שנוכל לבנות את הספריה.

Java

בשביל להפעיל את הפונקציה יש לטעון את הספריה באפליקציה עם השם שנתנו ב Android.mk, אח"כ נקרא לה ונקבל ממנה מחרוזת:

package legend.proxytype.ndktest3;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {

    //loading the library with the name from android.mk
    static
    {
       System.loadLibrary("MyLib");
    }

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

        TextView lbl_jni = (TextView)findViewById(R.id.lbl_jni);
        //make the call to function
        lbl_jni.setText(getStringFromNative());

    }

    public native String getStringFromNative();
}


Gradle

אם חשבתם שסיימנו אז עוד קצת וזה נגמר, יש לערוך את קובץ build.gradle על מנת שיבנה את הספריה באופן אוטומטי בכל בנייה מחדש של האפליקציה, יש לקרוא ל Module עם אותו שם של הספריה, ולהגדיר Task שיקרא ל Shell שיפעיל את ה NDK:

import org.apache.tools.ant.taskdefs.condition.Os

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "legend.proxytype.ndktest3"
        minSdkVersion 14
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

        ndk
         {
          //same as the name in android.mk
           moduleName "MyLib"
         }
    }

    sourceSets.main {
        jniLibs.srcDir 'src/main/libs'
        jni.srcDirs = [] 
    }

    //set task for the compilation of the library
    task ndkBuild(type: Exec) {
        //hadling different host machine
        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
            commandLine('ndk-build.cmd', '-C' , file('src/main').absolutePath)
        } else {
            commandLine 'ndk-build', '-C', file('src/main').absolutePath
        }
    }

    //the build of the application depend on the success of the JNI
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkBuild
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.0'
}

דבר אחרון לפני סיום הוא לוודא שתיקיית ה NDK מוגדרת ב Local.Properties של Gradle:

ndk.dir=E\:\\Users\\proxytype\\AppData\\Local\\Android\\ndk



מבנה האפליקציה אחרי כל הבלאגן

סיכום

הכנת סביבה לתמיכה ב NDK זה עסק לא פשוט, למרות שיש דיבורים להפוך אותו לחלק בלתי נפרד בגרסאות הבאות של Android Studio, מי יודע מתי זה יקרה, אבל האפשרויות שהוא נותן שווים את המאמץ ויש דברים שאפשר לעשות רק איתו כפי שנראה עוד בהמשך.

100% טבעי...

יום שבת, 7 במרץ 2015

Android Music Player



אחרי חופשה של מספר חודשים אפשר להתחיל את השנה החדשה עם מאמר שבו אלמד כיצד לבנות נגן MP3 בסביבת Android, הרעיון מאוד פשוט, האפליקציה תסרוק את המכשיר עבור קבצי מוזיקה ותנגן אותם אחד אחרי השני עם פונקציות שליטה בסיסיות.

שימו לב!

  • המחלקה נכתבה על Android Studio.
  • המחלקה תואמת ל Api 14.
Music Search

נקודת ההתחלה עבור האפליקציה היא קודם כל לחפש את כל קבצי המוזיקה שעל המכשיר אבל לפני שנתחיל נגדיר מחלקה פשוטה שתאחסן בשבילנו את הנתונים:

    private class SongDetails {
        String songTitle = "";
        String songArtist = "";
        //song location on the device
        String songData = "";
    }

לאחר מכן נפעיל את הפונקציה שתתחבר למסד הנתונים במכשיר ותבקש את כל הקבצי מדיה שמוגדרים כמוזיקה (מתעלמים לחלוטין מוידאו), התוצאה תהיה טבלה שתכיל עמודות לפי מה שביקשנו בשאילתא:

 private void getAllSongs()
    {
       //creating selection for the database
        String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
        final String[] projection = new String[] {
                MediaStore.Audio.Media.DISPLAY_NAME,
                MediaStore.Audio.Media.ARTIST,
                MediaStore.Audio.Media.DATA};
        
        //creating sort by for database
        final String sortOrder = MediaStore.Audio.AudioColumns.TITLE
                + " COLLATE LOCALIZED ASC";

        //stating pointer
        Cursor cursor = null;

        try {
            //the table for query
            Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
            // query the db
            cursor = getBaseContext().getContentResolver().query(uri,
                    projection, selection, null, sortOrder);
            if (cursor != null) {
                
                //create array for incoming songs
                songs = new ArrayList<SongDetails>(cursor.getCount());
                
                //go to the first row
                cursor.moveToFirst();

                SongDetails details;

                while (!cursor.isAfterLast()) {
                    //collecting song information and store in array,
                    //moving to the next row
                    details = new SongDetails();
                    details.songTitle = cursor.getString(0);
                    details.songArtist = cursor.getString(1);
                    details.songData = cursor.getString(2);
                    songs.add(details);
                    cursor.moveToNext();
                    
                }
            }
        } catch (Exception ex) {

        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }



Simple Player

עד עכשיו היה החלק הטכני שבו אנחנו מקבלים את חומרי הגלם ועכשיו אפשר להתחיל לעבוד איתם בעזרת מחלקה מאוד פשוטה שעושה פעולות (Stop, Play, Pause, Forward, Backward, Seek) כמו כל נגן סטנדרטי , על מנת לעדכן את ה Gui נעזר ב Interface משותף כפי שניתן לראות בדוגמה:

public interface OnPlayerEventListener {
    void onPlayerComplete();
    void onPlayerStart(String Title,int songDuration,int songPosition);
}


המחלקה מקבלת את המופע של ה Activity שמפעיל אותה, מאתחלים אותה עם רשימת השרים ששלפנו מה Database ומחכים לפקודות מה Gui.

public class simplePlayer
{
    OnPlayerEventListener mListener;
    Activity mActivity;

    //give access to the gui
    public ArrayList<songDetails> songs = null;
    public boolean isPaused = false;
    public int currentPosition = 0;
    public int currentDuration = 0;

    //single instance
    public static MediaPlayer player;

   //getting gui player interface
   public  simplePlayer(Activity ma)
   {
       mActivity = ma;
       mListener = (OnPlayerEventListener) mActivity;
   }

    //initialize the player
    public void init(ArrayList<songDetails>_songs)
    {
        songs = _songs;
        currentPosition = 0;


        if(player == null)
        {
            player = new MediaPlayer();
            player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {

                    player.stop();
                    player.reset();

                    nextSong();
                    mListener.onPlayerSongComplete();

                }
            });
        }
    }

    //stop the current song
    public void stop()
    {
        if(player != null)
        {
            if(player.isPlaying())
                //stop music
                player.stop();

            //reset pointer
            player.reset();
        }
    }

    //pause the current song
    public void pause()
    {
        if(!isPaused && player != null)
        {
            player.pause();
            isPaused = true;
        }
    }

    //playing the current song
    public void play()
    {
        if(player != null)
        {
            if(!isPaused && !player.isPlaying())
            {
                if(songs != null)
                {
                    if(songs.size() > 0)
                    {
                        try {
                            //getting file path from data
                            Uri u = Uri.fromFile(new File(songs.get(currentPosition).songData));

                            //set player file
                            player.setDataSource(mActivity,u);
                            //loading the file
                            player.prepare();
                            //getting song total time in milliseconds
                            currentDuration = player.getDuration();
                          
                            //start playing music!
                            player.start();
                            mListener.onPlayerSongStart("Now Playing: "
                                    + songs.get(currentPosition).songArtist
                                    + " - "+ songs.get(currentPosition).songTitle
                                    ,currentDuration,currentPosition);
                        }
                        catch (Exception ex)
                        {
                            ex.printStackTrace();
                        }
                    }
                }
                else
                {
                    //continue playing, reset the flag
                    player.start();
                    isPaused = false;
                }
            }
        }
    }

    //playing the next song in the array
    public  void nextSong()
    {
        if(player != null)
        {
            if(isPaused)
                isPaused = false;

            if(player.isPlaying())
                player.stop();

            player.reset();

            if((currentPosition + 1) == songs.size())
                currentPosition = 0;
            else
                currentPosition  = currentPosition + 1;

            play();
        }
    }

    //playing the previous song in the array
    public void previousSong()
    {
        if(player != null)
        {
            if(isPaused)
                isPaused = false;

            if(player.isPlaying())
                player.stop();

            player.reset();

            if(currentPosition - 1 < 0)
                currentPosition = songs.size();
            else
                currentPosition = currentPosition -1;

            play();
        }
    }

    //getting new position for playing by the gui seek bar
    public void setSeekPosition(int msec)
    {
        if(player != null)
            player.seekTo(msec);
    }

    //getting the current duration of music
    public int getSeekPosition()
    {
        if(player != null)
            return  player.getDuration();
        else
            return -1;
    }
}

ה Activity ממש את ה Interface, ומחפש שירים על המכשיר ומכניס אותם למערך שנשלח למחלקה, ניתן ליצור כפתורים ולשלוט במחלקה בעזרת Gui:

public class MainActivity extends ActionBarActivity 
implements OnPlayerEventListener {

    simplePlayer player;

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

        //getting all songs in the device
        getAllSongs();

        if (player == null) {
            //create new instance and send the listener
            player = new simplePlayer(this);
            //initialize the simplePlayer
            player.init(songs);
            //start playing the first song
            player.play();

            seekBar.setMax(player.currentDuration);
        }
    }

    @Override
    public void onPlayerSongComplete() {

    }

    @Override
    public void onPlayerSongStart(String Title, int songDuration, int songPosition) {
        this.setTitle(Title);
        seekBar.setMax(songDuration);
        seekBar.setProgress(0);
    }
}

סיכום:

דרך פשוטה מאוד שבעזרת ניתן לנגן מוזיקה במכשיר וזה רק חלק קטן מכלי המדיה המגוונים ש Android נותן, על הדרך ראינו כיצד לממש ממשק בין ה GUI למחלקה שיגרום לאירוע כאשר שיר נגמר ומתחיל.

אל תפריעו לשכנים...

יום שבת, 10 במאי 2014

WebView Interfaces




כשניגשים לפתח אפליקציה נשאלת השאלה מה הפלטפורמה המתאימה ביותר?, חלק יגידו שכתיבה מסורתית בקוד Native היא הפתרון המתאים ויש כאלה שחולקים על זה ומעדיפים לכתוב ב HTML 5  במעטפת Webkit ותוספות נוספות כמו PhoneGap, האמת שככל שעובר זמן יש יתרון בולט ל Html 5 כי אינו מצריך התייחסות מיוחדת כלפי מערכת ההפעלה והופך את האפליקציה ל Cross Platform אבל ברגע שצריכים לבצע פעולות מיוחדות בחומרה כל העסק מתחיל להתפרק כמו מגדל קלפים ברוח.

אופי האפליקציה חשוב מאוד לבחירת הפלטפורמה למשל אם זו תוכנית לתצוגת נתונים מ Database שמזכירה בהתנהגות שלה אתר אינטרנט מומלץ להשתמש ב Html 5, זו הדרך הפשוטה והמהירה ביותר, אבל אם רוצים להשתמש במצלמה או בכל רכיב חומרתי אחר Native Code חזק יותר אז מה עושים עם אפליקציות שקשה להגדיר? נקודת המוצא היא להשתמש ב Native אבל אם ניתן להגדיר תחומי אחריות ברורים ניתן לשלב גם Html וכך להקטין את התלות במערכות ההפעלה.

סביבות משולבות

ההתממשקות הראשונה היא בסיסית ביותר ונעשת בעזרת האזנה מאחורי הקלעים ל Redirects שקורים ב WebKit ומאפשרת לשנות את ההתנהגות של האפליקציה, הדוגמה מתחילה מ Activity שמכיל בתוכו כפתור, לחיצה עליו מעבירה ל Activity אחר עם Webview שטוען דף Html עם מספר קישורים שמובילים לאיזורים שונים באפליקציה:



BrowserActivity

כשמתמקדים בקוד של ה Activity ניתן לראות כיצד אפשר לדרוס את הפונקציה המקורית של ה WebKit בפונקציה חדשה דרכה ניתן לנתח את הבקשות של המשתמש.

import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class BrowserActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browser);
WebView _view = (WebView)findViewById(R.id.webView1);
class MainScreenActivity extends WebViewClient {

//every redirect active this function
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//checking the url and change behavior
if(url.toLowerCase().contains("back:mainactivity"))
{
finish();
return true;
}
else if(url.toLowerCase().contains("goto:otheractivity"))
{
Intent intent = new Intent(BrowserActivity.this,OtherActivity.class);
startActivity(intent);
return true;
}
else
{
//send back to the original call
return super.shouldOverrideUrlLoading(view, url);
}
}
}
//overwrite the WebViewClient class with new implementation 
_view.setWebViewClient(new MainScreenActivity());
//make the first call
_view.loadUrl("file:///android_asset/www/index.html"); 
}

}


JavaScript Interface

ההתממשקות הבאה יותר מורכבת ומאפשרת לקרוא לפונקציות Native בעזרת פונקציות JavaScript , יוצרים מחלקה ייעודית שדרכה נוצר גשר שמחבר בין 2 הפלטפורמות.

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.webkit.WebView;

public class BrowserActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browser);
WebView _view = (WebView)findViewById(R.id.webView1);
//add javascript support to webview
_view.getSettings().setJavaScriptEnabled(true);
//interface implementation
        class jsInterface{
        Context mContext;
        String mMsg;
     
          public jsInterface(Context m) {
          mContext = m;
        }
   
         //@JavascriptInterface
         //add in higher sdk version  4.0.1 +
         public void SetInterface(String Msg)
         {
            mMsg = Msg;
         }
   
            //@JavascriptInterface
          //add in higher sdk version  4.0.1 +
          public String GetInterface()
         {
            return mMsg;
          }
   
}
//add interface to webview, with bridge name
_view.addJavascriptInterface(new jsInterface(this), "Android");
//make the first call
_view.loadUrl("file:///android_asset/www/index.html"); 
}

}

קובץ ה JavaScript קורא לפונקציות החשופות בעזרת השם של הממשק:

function setMessage()
{
Android.SetInterface(document.getElementById('txt_msg').value);
}

function getMessage()
{
document.getElementById('lbl_msg').innerHTML = Android.GetInterface();
}



סיכום

אני לא יודע מה יקרה בעתיד אבל הדילמה אם להשתמש ב Native או ב Html 5 עדיין קיימת ומצריכה חשיבה מחוץ לקופסה,המון חברות לקחו את הרעיון של ה HTML5 ויצרו סביבות פיתוח שלמות שמבוססות על הרעיון, אם זו השיטה הטובה ביותר, לא תמיד ולפעמים אין ברירה ולדבר עם המכשיר בשפה שהוא מכיר.

מידע נוסף

בהצלחה...

יום שבת, 19 באוקטובר 2013

Android OpenGL And Gyroscope



אחרי שהכרנו כיצד לבנות ולצבוע אובייקטים השלב הבא הוא תנועה במרחב של המנוע אבל לפני שניכנס לכפתורים ולממשק המשתמש נתחיל דווקא עם החיישנים שנמצאים בכל מכשיר מתקדם, אבל חשוב לזכור שהם משתנים ממכשיר למכשיר ולא זמינים בכולם.

שימו לב! - המאמר מתבסס על Samsung Galaxy S3 שבו יש Gyroscope מובנה.

Gyroscope

מכשיר מדידה שמאפשר לחשב את זווית הסטייה כאשר הגוף לא במצב אופקי, כלומר כל הטיה של המכשיר תשנה את המיקום על 3 הצירים התלת מימדיים (X,Y,Z),  על מנת להפעיל אותו ב Android עלינו להשתמש במספר מחלקות שיעשנו לנו את העבודה עבור כל החיישנים שנראה במאמרים הבאים:

private SensorManager mSensorManager;
private Sensor mSensor;
אובייקטים שמייצגים את החיישנים.

mSensorManager = (SensorManager)con.getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);

מבקשים  את השירות של החיישנים במערכת, מתחברים לחיישן ספציפי שבמקרה שלנו הוא ה Gyroscope אבל ניתן לקבל חיישנים נוספים כמו: Accelerometer, Ambient Temperature,Gravity ועוד (לרשימה המלאה).

לאחר מכן מתחברים ל Event של החיישן בעזרת SensorEventListener ודוגמים אותו, השתמשתי בהגדרה קיימת במערכת שמתאימה למשחקים ודוגמת את החיישן כל 20 ms.

mSensorManager.registerListener(new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
// TODO Auto-generated method stub
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO Auto-generated method stub
}
}, mSensor, SensorManager.SENSOR_DELAY_GAME);


OpenGL

אחד השינויים הוא בפונקציה Draw של האובייקט שמוכרת מהמאמרים האחרונים בנושא, נוספו 3 פרמטרים עבור X,Y,Z שאותם נשלח ל OpenGL בעזרת הפונקציה glTranslatef.

public void draw(float x,float y,float z)
{
//set gl focus to cube
gl.glLoadIdentity();
gl.glTranslatef(x, y, z);
   //...
}

קוד


דגימת החיישן מתבצעת ה GLSurfaceView.Renderer בעזרת EventListener שממתין לאירוע מהחיישן ומעדכן פרמטרים גלובלים במחלקה שנשלחים אח"כ לפונקציה Draw של האובייקט.



package com.example.openglgyroscope;

import java.text.DecimalFormat;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;


public class OpenGLSensorRender implements GLSurfaceView.Renderer  {


private CubeNaked mCube;
//remember the last values of the Axis
private float mLastX = 0, mLastY = 0, mLastZ = 0;
Context con;
private SensorManager mSensorManager;
private Sensor mSensor;
public OpenGLSensorRender(Context _con)
{
con = _con;
mSensorManager = (SensorManager)con.getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
mSensorManager.registerListener(new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
//taking sample from the sensor
DecimalFormat _fp = new DecimalFormat("#.#");
float x = Float.valueOf( _fp.format(event.values[0]));
float y = Float.valueOf( _fp.format(event.values[1]));
float z = Float.valueOf( _fp.format(event.values[2]));
mLastX = mLastX + x;
mLastY = mLastY+ y;
   //not in use for now
//mLastZ = z;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}


}, mSensor, SensorManager.SENSOR_DELAY_GAME);
}
@Override
public void onDrawFrame(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
//draw the cube ,sending the new axis
mCube.draw(mLastX,mLastY,mLastZ);
}

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
//enter to Perspective Projection Mode Settings
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f,
100.0f);
gl.glViewport(0, 0, width, height);
//enter to ModelView Projection Mode Settings
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
//clear the suraface color to black
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
mCube = new CubeNaked(gl);
}

}


סרט הדגמה





קבצי מקור


סיכום:

עבודה עם חיישנים נפוצה מאוד במיוחד במשחקים, הם מאפשרים לנו לשלוט על האובייקטים במשחק מבלי שנצטרך לגעת במסך שגם ככה יחסית קטן וחוסכים לנו ממשק משתמש שמתלבש על המסך שעלול להסתיר חלקים מהמשחק, אבל צריך להתרגל לצורת המשחק החדשה ולא תמיד זה נוח למי שרגיל לכפתורים הסטנדרטים.

שחק איתו!

יום חמישי, 29 באוגוסט 2013

Android OpenGL UV Mapping





ראינו שלא פשוט לבנות קובייה ועוד יותר מסובך לצבוע אותה, אבל שוב חוזרים לאותה שאלה מה קורה כאשר האובייקט מורכב מיותר מ 6 משטחים? גם הפעם עולם התלת מימד נותן לנו כלים להתמודד עם אובייקטים מורכבים בעזרת שיטת ה UV Map שמאפשרת לצבוע אותם בצורה יעילה וחסכונית.

UV Map

שיטה לפריסת אובייקט תלת מימדי לדו מימד, עד עכשיו חיברנו משטח בודד לטקסטורה בודדת שזה לא הפתרון המושלם והוא בזבזני ולא ידידותי, בעזרת UV Coordinates ניתן לחבר מספר משטחים לנקודות ספציפיות בטקסטורה עצמה כך שכל משולש \ מרובע יצייר רק את התחום שלו, עוד יתרון מאוד חושב שניתן לשלב באותה טקסטורה מספר אבייקטים שונים ולחסוך מקום וקבצים.  

חבית בדו מימד
ניתן לראות שכל משטחי החבית נפרסים על על תמונה בודדת עם קואורדינטות עבור כל Vertex, התמונה תתלבש על האובייקט כשנטען אותה ל OpenGL, זו שיטה שמתאימה למשחקים ומאוד פופלרית, חשוב מאוד לדעת שיש חלק מאוד אומנותי שמצריך קישורי ציור בתהליך הכנת הטקסטורה ויש המון שיטות וטכניקות בקיצור עולם ומלואו ,מי שמעוניין להיכנס אליו יותר לעומק מומלץ למצוא תוכנת תלת מימד ולחפור אותה עד שיצא עשן ולמי שזה לא מתאים לא חסר מודלים שמוכנים מראש.


חבית בתוכנת תלת מימד


*.OBJ


לפני שניכנס לפורמט פעם נוספת חשוב להדגיש שחלק מתוכנות התלת מימד שומרות בקובץ את המידע של ה Vertex כמרובע ולא כמשולש, ברוב המקרים זה בסדר אבל בגלל שאנחנו עובדים על OpenGL ES שמותאם ל Android ולא ניתן לעבוד עם מרובעים, יש להמיר את האובייקט למשולשים ורק אח"כ לשמור את המידע לקובץ, במקרה שלי Modo שומרת את המשטחים כמרובעים ללא אפשרויות נוספות, נאלצתי להוריד סקריפט מיוחד שיעשה זאת, להורדת הסקריפט לחץ כאן.

קובץ ה Obj מכיל את המידע בנוגע לאובייקט, למען הסדר הטוב אני יחזור על דברים שנאמרו באחד המאמרים בנושא על מנת שנשמור על הרצף,בתחילת הקובץ מופיעות נקודות ה Vertex של האובייקט עצמו עם סימון V מדובר על X Y Z עבור כל נקודה, לאחר מכן מגיע רצף נקודות ה Normal שעליהם נפרט במאמרים הבאים.
v -0.0200 0.0400 -1.0200
vn -0.4004 -0.7179 -0.5695

לאחר הקואורדינטות של האובייקט עצמו מתחיל רצף הנקודות של הטקסטורה, בדומה למאמר האחרון בו מערך הקואורדינטות היה כחלק מהקוד במקרה הזה אנחנו שואבים אותו מהקובץ אבל קיים הבדל בהתייחסות בין הסטנדרט של OBJ לבין OpenGL, הקובץ מתייחס לקוארדינטה 0.0 ל Top Left בזמן ש OpenGL מתייחס לקוארדינטה כ Bottom Left לכן עלינו להפוך את הטקסטורה כפי שנראה בקוד.

vt 0.5646 0.9954

הפורמט Obj מכיל בתוכו קישור לקובץ ה Material שמכיל בתוכו קישור לטקסטורה ופרמטרים נוספים.

mtllib barrel1.mtl

בסוף הקובץ מופעים האינדקסים של המשטחים שנצייר על המסך, לכל משטח יש 3 אינדקסים שמצבעים על הנתונים במערכים שנטענו לפני שהגענו לאנידקסים (V,VN,VT) , ניתן לראות בדוגמה שלמשטח באינדקס 20 תואמות הקואורדינטות במיקום 1 במערך של ה Normal Vertex ולמיקום 18 במערך של ה Texture Vertex, אבל גם פה יש מגבלה של ה OpenGL, לא ניתן לעבוד עם יותר מאינדקס אחד ועלינו להכין מראש מערך מסודר שיטען ברצף את הקוארדינטות כפי שנראה בהמשך.

f 20/1/18 8/2/7 21/3/19


ObjectParser

התוכנית עוברת מתיחת פנים מהפעם האחרונה,נוספו מחלקות חדשות ומספר פונקציות נוספות על מנת להשלים את התהליך,  יש לסדר את האינדקסים ולהכין את ה Buffers לקראת הכנסה ל OpenGL, נא לעקוב אחרי ההערות בקוד.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Vector;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Log;

public class ObjectParser {

//support class
public class vtPoints {
public float V, T;
}

//support class
public class vnPoints {
public float N1, N2;
}

//support class
public class vLayer {
public float X, Y, Z;
public vtPoints vt = new vtPoints();
public vnPoints vn = new vnPoints();
}

//array contains all the sorted information
private ArrayList<vLayer> vSlice = new ArrayList<vLayer>();

// arrays contains all points (vertex/normal)
private ArrayList<vtPoints> vtSlice = new ArrayList<vtPoints>();
private ArrayList<vnPoints> vnSlice = new ArrayList<vnPoints>();

//arrays contains all the indices information
private Vector<Short> vPointer = new Vector<Short>();
private Vector<Short> vtPointer = new Vector<Short>();
private Vector<Short> vnPointer = new Vector<Short>();

//buffers to load to opengl
private FloatBuffer vertexBuffer;
private ShortBuffer faceBuffer;
private FloatBuffer textureBuffer;
private FloatBuffer normalBuffer;

//reader of the file
private BufferedReader reader;

private Context ctx;
private GL10 gl;
private float mCubeRotation;

//textures pointers array
private int texturesPointers[];

public ObjectParser(Context context, String _filename, GL10 _gl) {
gl = _gl;
ctx = context;
try {
reader = new BufferedReader(new InputStreamReader(ctx.getAssets()
.open(_filename)));
} catch (IOException e) {
e.printStackTrace();
}

loadFile();
sortArrays();
}

public void Draw() {

gl.glLoadIdentity();
gl.glTranslatef(0.1f, -0.1f, -10.0f);

// rotate the object
gl.glRotatef(mCubeRotation, 0.0f, 0.5f, 0.1f);
gl.glEnable(GL10.GL_TEXTURE_2D);

//load buffers
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
gl.glNormalPointer(GL10.GL_FLOAT, 0, normalBuffer);

gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

if (texturesPointers[0] != 0) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, texturesPointers[0]);
}

gl.glDrawElements(GL10.GL_TRIANGLES, faceBuffer.capacity(),
GL10.GL_UNSIGNED_SHORT, faceBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
   gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);

mCubeRotation -= 0.25f;
}

private void sortArrays() {
for (int i = 0; i < vPointer.size(); i++) {

// get the vertex by the obj face indices
vLayer _layer = (vLayer) vSlice.get(vPointer.get(i));

// get the texture coordinates from obj texture indices
vtPoints _vt = (vtPoints) vtSlice.get(vtPointer.get(i));

// get the normal coordinates from obj texture indices
vnPoints _vn = (vnPoints) vnSlice.get(vnPointer.get(i));

// update the vertex layer with the new coordinates
_layer.vt = _vt;
_layer.vn = _vn;

}

//rearrange buffers by indices
Vector<Float> v = new Vector<Float>();
Vector<Float> vt = new Vector<Float>();
Vector<Float> vn = new Vector<Float>();

for (int i = 0; i < vSlice.size(); i++) {

vLayer _layer = vSlice.get(i);

v.add(_layer.X);
v.add(_layer.Y);
v.add(_layer.Z);

vt.add(_layer.vt.V);
vt.add(_layer.vt.T);

vn.add(_layer.vn.N1);
vn.add(_layer.vn.N2);

}

loadBuffer(v, vt, vn, vPointer);

}

private void loadBuffer(Vector<Float> v, Vector<Float> vt,
Vector<Float> vn, Vector<Short> f) {

//buffer for vertex
ByteBuffer byteBuf = ByteBuffer.allocateDirect(v.size() * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(toPrimitiveArrayF(v));
vertexBuffer.position(0);

//buffer for texture
byteBuf = ByteBuffer.allocateDirect(vt.size() * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(toPrimitiveArrayF(vt));
textureBuffer.position(0);

//buffer for normal
byteBuf = ByteBuffer.allocateDirect(vn.size() * 4 * 3);
byteBuf.order(ByteOrder.nativeOrder());
normalBuffer = byteBuf.asFloatBuffer();
normalBuffer.put(toPrimitiveArrayF(vn));
normalBuffer.position(0);

//buffer for indices -> faces
ByteBuffer fBuf = ByteBuffer.allocateDirect(f.size() * 2);
fBuf.order(ByteOrder.nativeOrder());
faceBuffer = fBuf.asShortBuffer();
faceBuffer.put(toPrimitiveArrayS(f));
faceBuffer.position(0);
}

private void loadFile() {
String line = null;
try {
while ((line = reader.readLine()) != null) {

// faces (3 indices)
if (line.startsWith("f")) {
processFace(line);
Log.d("Face Loaded", line);
}

//texture vertices
else if (line.startsWith("vt")) {
processVT(line);
Log.d("Texture coordinates Loaded", line);
}

//nornal vertices
else if (line.startsWith("vn")) {
processVN(line);
Log.d("Normal coordinates Loaded", line);
}

// vertices
else if (line.startsWith("v")) {
processV(line);
Log.d("Vetex Loaded", line);
}

//material
else if (line.startsWith("mtllib")) {
loadMaterial(line);
Log.d("Material Loaded", line);
}
}
} catch (IOException e) {
e.printStackTrace();
}

}

// load material file
private void loadMaterial(String line) {

String splitter[] = line.split(" ");
String filename = splitter[1];
BufferedReader textureReader;
try {
textureReader = new BufferedReader(new InputStreamReader(ctx
.getAssets().open(filename)));
String tLine = null;

while ((tLine = textureReader.readLine()) != null) {
if (tLine.startsWith("map_Kd")) {
loadTexture(tLine.split(" ")[1]);
}
}

} catch (IOException e) {
e.printStackTrace();
}

}

// load bitmap from the material file
private void loadTexture(String TextureMap) {
Bitmap tMap = null;
try {
// flip image because the different 
//sorting between OpenGL and OBJ standards
tMap = BitmapFactory.decodeStream(ctx.getAssets().open(TextureMap));
Matrix flip = new Matrix();
flip.postScale(1f, -1f);
Bitmap bitmap = Bitmap.createBitmap(tMap, 0, 0, tMap.getWidth(),
tMap.getHeight(), flip, true);
tMap = bitmap;
} catch (IOException e) {
e.printStackTrace();
return;
}

texturesPointers = new int[1];
GLES20.glGenTextures(texturesPointers.length, texturesPointers, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, texturesPointers[0]);

// texture behavior
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);

// insert the bitmap into open openGL buffer
gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,
GL10.GL_DECAL);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, tMap, 0);

// no need this anymore...
tMap.recycle();

}

// export indices
private void processFace(String line) {

String[] tokens = line.split("[ ]+");
int c = tokens.length;

Vector<Short> tmpFaces = new Vector<Short>();
Vector<Short> tmpVn = new Vector<Short>();
Vector<Short> tmpVt = new Vector<Short>();
for (int i = 1; i < tokens.length; i++) {

// vertices indices
Short s = Short.valueOf(tokens[i].split("/")[0]);
s--;
tmpFaces.add(s);

// texture indices
s = Short.valueOf(tokens[i].split("/")[1]);
s--;
tmpVt.add(s);

// normal indices
s = Short.valueOf(tokens[i].split("/")[2]);
s--;
tmpVn.add(s);

}
vPointer.addAll(tmpFaces);
vtPointer.addAll(tmpVt);
vnPointer.addAll(tmpVn);

}

// export vertex coordinates
private void processV(String line) {

// split space
String[] tokens = line.split("[ ]+");
vLayer _layer = new vLayer();

int c = tokens.length;
for (int i = 1; i < c; i++) {

switch (i) {
case 1:
_layer.X = Float.valueOf(tokens[i]);
break;
case 2:
_layer.Y = Float.valueOf(tokens[i]);
break;
case 3:
_layer.Z = Float.valueOf(tokens[i]);
break;
default:
break;
}

}
vSlice.add(_layer);
}

// export normal coordinates
private void processVN(String line) {
String[] tokens = line.split("[ ]+");
int c = tokens.length;
vnPoints _layer = new vnPoints();
for (int i = 1; i < c; i++) {

switch (i) {
case 1:
_layer.N1 = Float.valueOf(tokens[i]);
break;
case 2:
_layer.N2 = Float.valueOf(tokens[i]);
break;
default:
break;
}

}
vnSlice.add(_layer);
}

// export texture coordinates
private void processVT(String line) {
String[] tokens = line.split("[ ]+");
int c = tokens.length;
vtPoints _layer = new vtPoints();
for (int i = 1; i < c; i++) {

switch (i) {
case 1:
_layer.V = Float.valueOf(tokens[i]);
break;
case 2:
_layer.T = Float.valueOf(tokens[i]);
break;
default:
break;
}

}

vtSlice.add(_layer);
}

// convert Short Vector to Short Array
private short[] toPrimitiveArrayS(Vector<Short> vector) {
short[] s;
s = new short[vector.size()];
for (int i = 0; i < vector.size(); i++) {
s[i] = vector.get(i);
}
return s;
}

// convert Float Vector to Float Array
private float[] toPrimitiveArrayF(Vector<Float> vector) {
float[] f;
f = new float[vector.size()];
for (int i = 0; i < vector.size(); i++) {
f[i] = vector.get(i);
}
return f;
}

}


המחלקה מקבלת את שם הקובץ ופותחת אותו מתיקיית ה Assets לאחר מכן עוברת שורה שורה בקובץ (LoadFile) ומוצאת את הנתונים של ה Normal, Vertex ו Texture, אוספת את מיקום הטקסטורה מקובץ ה Material ולסיום אוספת את האינדקסים לאחר מכן מייצרת מערך מסוג vLayer עבור שורות ה Vertices , וטוענת לתוכו את הקוראדינטות של ה Normal וה Texture ע"פ האידקסים שלהם.

דוגמה מהמכשיר

חבית במכשיר

הורדת הפרוייקט


סיכום

ראינו כמה העסק מסובך אבל מאוד מאתגר, עכשיו אפשר לתכנן אובייקטים מורכבים ולצבוע אותם,ניתן להתחיל להכין את הסביבה עבור משחק התלת מימד הראשון שנעשה באנדרואיד.

בהצלחה.