2015年3月31日 星期二

[Android] About Service - how to establish?

In general, we use Activity to construct our program, including the interaction with user and what need to do.
So...why do we need what called Service?

Image that you may need to play music in background or communicate through TCP/UDP and so on.
Of course, you can new a thread to run these jobs in background.
But, what if we want these functions to be available even we have closed our activity?
In this case, you will need the Service mechanism.

First, we need to create service class:
public class myService  extends Service {
 private ServiceThread mServiceThread; 
 @Override
 public IBinder onBind(Intent intent) {
  // TODO Auto-generated method stub
  return null;
 }
 
 @Override
 public void onCreate() {
 // TODO Auto-generated method stub
 super.onCreate();
 }
  
 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  mServiceThread = new ServiceThread();
  mServiceThread.start();
     return  START_STICKY ; 
 }     
  

 @Override
 public void onDestroy() {
  Log.d(TAG,"onDestroy()");
  if (mServiceThread != null)
   mServiceThread.interrupt();
 }
  
 private class ServiceThread extends Thread {
  @Override
  public void run() {
   super.run();
   //your action here
  }
 } 
}
We new  ServiceThread() here because we need our service to run some jobs repeatedly.
If you only want to do some simple actions and just need to do them once, you can add the related codes in onStartCommand() directly.

After that, we need to register our service in AndroidManifest.xml.
<service android:name="myPackageName.myService"> </service >
We have established our Service now.

How to call it?
Assume that you have on button in the activity, and we want to start the service when we press it.
public void onClick(View v) {
Intent i = new Intent(this, myService.class);  
startService(i);
}
On the other hand, if we want to stop the Service?
public void onClick(View v) {
Intent i = new Intent(this, myService.class);  
stopService(i);
}

To check if we have established our service or not, we can open the RUNNING list in Settings/Apps.
There should have the name of our service.

[C#] How to minimized to tray?

When we want to our program to minimized to tray, like the resident program,
we can use the "NotifyIcon" control in the toolbox.
Besides, we also need the tool "ContextMenuStrip" since we may want to restore our program after it has been minimized.

First, add one "ContextMenuStrip" control to your form.
We just add few menu items in it, say "Open" and "Quit" in our demo.
These items will be shown on the popup menu which is mouse right-click menu of our program when it is minimized to tray.



Then, add one "NotifyIcon" control to you form and edit its property.
The item Icon is the icon you will see when the program is in the tray.
Item Text is the popup message when th mouse move over it.
Click on the item ContextMenu, we can select the name of ContextMenuStrip we just added.

Now are the codes:
private void frmMain_Resize(object sender, EventArgs e)
        {
            if (this.WindowState == FormWindowState.Minimized)
            {
                this.notifyIcon1.Visible = true;
                this.Hide();
            }
            else
            {
                this.notifyIcon1.Visible = false;
            } 
        }

        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.Show();
            this.WindowState = FormWindowState.Normal; 
        }

        private void quitToolStripMenuItem_Click(object sender, EventArgs e)
        {            
            this.Close();            
        }
frmMain_Resize() is the Resize event function of the form.
openToolStripMenuItem_Click() is the Click Event function of "Open " item in ContextMenuStrip1.
quitToolStripMenuItem_Click() is the Click Event function of "Quit" item in ContextMenuStrip1.
That is.

Sometimes, we found that when we close some program, it was not terminated directly.
It would tell you that it is going to minimized to tray.
How did it do that?
 private void frmMain_FormClosing(object sender, FormClosingEventArgs e)
        {            
            DialogResult result;
            result = MessageBox.Show("Yes to minimize, No to quit directly, Cancel to return", "Minimize to tray?", MessageBoxButtons.YesNoCancel);
            if (result == DialogResult.Yes)
            {
                this.WindowState = FormWindowState.Minimized;
                e.Cancel = true;
            }            
            else if (result == DialogResult.Cancel)
            {                
                e.Cancel = true;
            }
            
        }
frmMain_FormClosing() is the FormClosing event function.
e.Cancel = true; means that the action of Closing is cancaled.

[C#] How to prevent our program from being executed twice?

When our program is running, and then the user press the execution file again,
we wish that it will show up the window of program being executed instead of forking a new program. 
To achieve that, we just need to check the status in program.cs:
static void Main(){           
    if (frmMain.AlreadyRunning())
    {
      return;
    }

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new frmMain());
}
The  function AlreadyRunning() is declared in frmMain.cs.
public static bool AlreadyRunning()
        {
            /*
            const int SW_HIDE = 0;
            const int SW_SHOWNORMAL = 1;
            const int SW_SHOWMINIMIZED = 2;
            const int SW_SHOWMAXIMIZED = 3;
            const int SW_SHOWNOACTIVATE = 4;
            const int SW_RESTORE = 9;
            const int SW_SHOWDEFAULT = 10;
            */
            const int swRestore = 9;

            var me = Process.GetCurrentProcess();
            var arrProcesses = Process.GetProcessesByName(me.ProcessName);

            if (arrProcesses.Length > 1)
            {
                for (var i = 0; i < arrProcesses.Length; i++)
                {
                    if (arrProcesses[i].Id != me.Id)
                    {
                        // get the window handle
                        IntPtr hWnd = arrProcesses[i].MainWindowHandle;

                        // if iconic, we need to restore the window
                        //if (IsIconic(hWnd))
                        {
                            MessageBox.Show("Already running", "", MessageBoxButtons.OK);
                            ShowWindowAsync(hWnd, swRestore);                            
                        }

                        // bring it to the foreground
                        SetForegroundWindow(hWnd);
                        
                        break;
                    }
                }
                return true;
            }

            return false;
        }

[C#] How to get and set System Time?

According to the formal website of Microsoft MSDN, it depicts how to get and set System Time as shown below:
[DllImport("coredll.dll")]
private extern static void GetSystemTime(ref SYSTEMTIME lpSystemTime);

[DllImport("coredll.dll")]
private extern static uint SetSystemTime(ref SYSTEMTIME lpSystemTime);

private struct SYSTEMTIME 
{
    public ushort wYear;
    public ushort wMonth; 
    public ushort wDayOfWeek; 
    public ushort wDay; 
    public ushort wHour; 
    public ushort wMinute; 
    public ushort wSecond; 
    public ushort wMilliseconds; 
}

private void GetTime()
{
    // Call the native GetSystemTime method
    // with the defined structure.
    SYSTEMTIME stime = new SYSTEMTIME();
    GetSystemTime(ref stime);

    // Show the current time.           
    MessageBox.Show("Current Time: "  + 
        stime.wHour.ToString() + ":"
        + stime.wMinute.ToString());
}
private void SetTime()
{
    // Call the native GetSystemTime method
    // with the defined structure.
    SYSTEMTIME systime = new SYSTEMTIME();
    GetSystemTime(ref systime);

    // Set the system clock ahead one hour.
    systime.wHour = (ushort)(systime.wHour + 1 % 24);
    SetSystemTime(ref systime);
    MessageBox.Show("New time: " + systime.wHour.ToString() + ":"
        + systime.wMinute.ToString());
}
However, when we execute it, we will encounter the error message "Unable to load DLL 'coredll.dll': The specified module could not be found".

To fix it, just import "kernel32.dll" instead of "coredll.dll", as shown below:
[DllImport("kernel32.dll")]
private extern static void GetSystemTime(ref SYSTEMTIME lpSystemTime);

[DllImport("kernel32.dll")]
private extern static uint SetSystemTime(ref SYSTEMTIME lpSystemTime);

[Android] How to carry files using Assets directory

Sometimes, we need to access our own files after installing the APP.
For example, we may want to play the default MP3 file in background.

The simple way is just put the files you want in the directory assets.
During your development stage, those files in this directory will not dealt with.
When you build the apk, they will be included at the same time.

Then, how can our program access them after installing the apk?
Some people may use the path "file://android_asset/" to access them.
However, the suggested method is to access them by AssetManager.

Assume that you put a file called "t.mp3" in the assets directory.
AssetManager assetManager = getAssets();     
InputStream in = null;
OutputStream out = null;
File sdCardDir = Environment.getExternalStorageDirectory();    
in = assetManager.open("t.mp3");
out = new FileOutputStream(new File(sdCardDir+"/myFolder", "t.mp3"));    
byte[] buffer = new byte[1024];
int read;
while((read = in.read(buffer)) != -1)
{
  out.write(buffer, 0, read);
}    
in.close();
in = null;
out.flush();
out.close();
out = null;

The method shown above is to copy the file "t.mp3" to directory "/myFolder".
Then we can access it by FileInputStream or another file access methods.

Of course, there have many different ways.
For example, if you want to play it directly:
AssetFileDescriptor fileDescriptor = getAssets().openFd("t.mp3");
MediaPlayer mp = new MediaPlayer();
mp.setDataSource(fileDescriptor.getFileDescriptor());

[Android] How to load my APP automatically after system booting up

In order to start the program automatically after system booting up, we need to:

Assume that MainActivity is the start up Activity.
We need to add a new class extended from BroadcastReceiver to accept the broadcasted messages.
 public class BootUpReceiverClass extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {      
         Intent i = new Intent(context, MainActivity.class);  
            i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(i);          
         }
    }

Then, we need to add a permission in AndroidManifest.xml.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

After that, we can a receiver tag within <application> </application>

   
       
       
      
    

[Android] MediaPlayer RTSP streaming

For MediaPlayer, if we want to play internet streaming,
we only need to set the internet address in setDataSource().
But, we if it needs account and password?
There are two possible types:
1. Basic Authentication

Context context = getApplicationContext();  
sUrl = "rtsp://xxxx";  
sAccount = "xx";  
sPassword = "xx";  
Uri source = Uri.parse(sUrl);  
Map<String, String> headers = getRtspHeaders();  
mediaPlayer.setDataSource(context, source, headers);

Map<String, String> getRtspHeaders() {  
  Map<String, String> headers = new HashMap<String, String>();  
   String basicAuthValue = getBasicAuthValue(sAccount, sPassword);  
   headers.put("Authorization", basicAuthValue);  
   return headers;  
}  
private String getBasicAuthValue(String usr, String pwd) {  
   String credentials = usr + ":" + pwd;  
   int flags = Base64.URL_SAFE | Base64.NO_WRAP;  
   byte[] bytes = credentials.getBytes();  
   return "Basic " + Base64.encodeToString(bytes, flags);       
}    

The above codes show us how to deal with basic authentication.
There has another simpler way - add the account and password in the path.
e.g.  sUrl = "rtsp://admin:pwd@xxxx";


2. Digest Authentication
Originally, I thought that we can use sUrl = "rtsp://admin:pwd@xxxx" for digest authentication as well.
However, I always got the error message "error 100, Media server died".
When I googled it, the answer was that "MedisPlayer does not support rtsp digest authentication" or something like that.
One day, I thought that I might use Wireshark to check the packets for what happened.
As the result shown above, it seems that the format is correct in RTSP DESCRIBE packet.
It means that it does support digest authentication type.
So...why did we get the reply "401 unauthorized"?
(The above graph may be captured not by using MediaPlayer. But the analysis discussed here is for MediaPlayer indeed)
Check the DESCRIBE packet again... username is correct, uri is correct, nonce is correct...
Is response the suspect?
response= md5(md5(username:realm:password):nonce:md5(uri))
I calculated it manually. It did have something wrong.
The response in DESCEIBE packet is not as what expected.
To find out the reason, I need to investigate the Android kernel codes.
The related codes is in ARTSPConnection.cpp.
Finally, I found that when it calculated response, the realm it used is not from RTSP server Reply packet.
Instead, it was hard coded as "Streaming Server".
No wonder the response was not correct.
Although the reason is clear, I can do nothing since I cannot rebuild my Android kernel.
It seems that I cannot use MediaPlayer for RTSP Digest Authentication
Need to find another way...

[Android] The basic concept of using MediaPlayer

If we want to play music or multimedia files, the simple way is to use MediaPlayer.
We can use in this way:

private MediaPlayer mp = new MediaPlayer();
mp.setDataSource("/sdcard/test.mp3");//Set the source
mp.prepare();
mp.start();

If the source is from internet streaming, it can also support it.
For example:
mp.setDataSource("rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov");
MediaPlayer support HTTP and RTSP streaming both.

If we use MediaPlayer to play video files, we will need to incorporate SurfaceView.
Notice that new MediaPlayer() must be declared in surfaceCreated(), as shown below:
@Overridepublic void surfaceCreated(SurfaceHolder sh) {
         mp= new MediaPlayer();
         mp.setDisplay(surfaceHolder);   
         mp.setOnPreparedListener(this);      
    }
@Override
     public void surfaceDestroyed(SurfaceHolder arg0) {
         mp.release();
     }

@Override
public void onPrepared(MediaPlayer mp) {
        mp.start();
     }
If we have one button to control the play action, we can add the codes below in onClick() event of that button.
mp.setDataSource("your source path");
mp.prepareAsync();  

MediaPlayer can support many formats, such as MP3,MPEG2,H.264,3gp....

[Android] 使用AudioTrack()會何聲音會播播停停?

使用AudioTrack()時,如果聲音有點過尖或過低
那就可能是格式設定有錯,或者sample長度給的不對,

但如果是播播停停,尤其是遇到類似以下的錯誤訊息
W/AudioTrack(..): releaseBuffer() track 0x6eaf04d0 name=0x1 disabled, restarting
該怎麼處理呢?

會遇到上面的訊息,表示資料給的太慢,
如果是local的檔案,通常不會有問題,
但如果是網路streaming,因為資料來的速率不一,
有時可能會有buffer underflow的問題,
有二個作法,一個是在前端網路接收端加buffer,
一個是在後端要進AudioTrack時加buffer,
底下示範的方法就是第二個方法,

private static void queueAudioData(byte[] buf, int size) {  
 if((audioPcmBufferDataCount + size)> audioBufferSize)
 {
  return;  
 }
 if((audioPcmBufferFront + size) > audioBufferSize)
 {
  //rewind
  System.arraycopy(buf, 0, audioPCMData, audioPcmBufferFront, audioBufferSize - audioPcmBufferFront);
  audioPcmBufferDataCount += (audioBufferSize - audioPcmBufferFront);
  size -= (audioBufferSize - audioPcmBufferFront);
  audioPcmBufferFront = 0;      
 }
 System.arraycopy(buf, 0, audioPCMData, audioPcmBufferFront, size);
 audioPcmBufferFront += size;
 audioPcmBufferDataCount += size;
} 

audio decoder解出來的PCM data,
先透過queueAudioData()存到一個queue buffer,
之後再穩定的將資料write到AudioTrack
private class playAudio extends Thread {
 @Override
 public void run() {  
  super.run();
  int len = 512;
  while(!isInterrupted()) {
   try {
    if(audioPcmBufferDataCount < len)
    {
     Thread.sleep(10);
     continue;
    }

    if((audioPcmBufferEnd + len) > audioBufferSize)
     writeSize = audioBufferSize - audioPcmBufferEnd;
    else
     writeSize = len;
    playAudioTrack.write(audioPCMData, audioPcmBufferEnd, writeSize);
    Thread.sleep(1);
    audioPcmBufferDataCount -= writeSize;
    audioPcmBufferEnd += writeSize;
    if(audioPcmBufferEnd >= audioBufferSize)
     audioPcmBufferEnd=0;
    
    if(playAudioTrack.getPlayState()!=AudioTrack.PLAYSTATE_PLAYING) {     
          playAudioTrack.play();
       }
   } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
   }       
  }
 }
}

[Android] MediaExtractor 和 MediaCodec 如何配合使用 - Audio篇

[Android] MediaExtractor 和 MediaCodec 如何配合使用 - Video篇裡,
我們示範了如何處理video,
現在我們來看audio部份,

原則上,我們可以使用同一個MediaExtractor來同時處理video/audio,
只要透過selectTrack()來動態切換即可,
不過,不知為何常常會收不到資料,
所以這邊我們就用固定的方式,
video和audio都有自己的MediaExtractor

audio處理方式和video大同小異,
除了audio不需要render到SurfaceView
以及audio需要多一個AudioTrack()來處理decode好的PCM data,

private MediaExtractor extractorAudio;
private MediaCodec decoderAudio;

extractorAudio = new MediaExtractor();
extractorAudio.setDataSource("myTest.mp4");

for (int i = 0; i < extractorAudio.getTrackCount(); i++) {
 MediaFormat format = extractorAudio.getTrackFormat(i);
 String mime = format.getString(MediaFormat.KEY_MIME); 
 if (mime.startsWith("audio/")) {  
  audioTrack = i;  
  extractorAudio.selectTrack(audioTrack);
  formatAudio = format;        
  decoderAudio = MediaCodec.createDecoderByType(mime);
  sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
  decoderAudio.configure(format, null, null, 0);
  break;
 }
}

if (audioTrack >=0) {
 if(decoderAudio == null)
 {
  Log.e(TAG, "Can't find audio info!");
  return;
 }
 else
 {
   // create our AudioTrack instance
   int minBufferSize = AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_CONFIGURATION_STEREO, AudioFormat.ENCODING_PCM_16BIT);
      int bufferSize = 4 * minBufferSize;
  playAudioTrack = new AudioTrack(
    AudioManager.STREAM_MUSIC,
    formatAudio.getInteger(MediaFormat.KEY_SAMPLE_RATE),
    AudioFormat.CHANNEL_OUT_STEREO,
    AudioFormat.ENCODING_PCM_16BIT,
    bufferSize,
    AudioTrack.MODE_STREAM
   );
  playAudioTrack.play();
  decoderAudio.start();
 }
}

extractorAudio一樣是根據MIME的資訊將audio track找出來,
decoderAudio.configure()我們只要傳進track format即可,其它參數都不需要,
另外,我們還需要多一個AudioTrack(),
因為decoderAudio最後解出來的是PCM data,
要實際發出聲音來,需要AudioTrack()幫忙,

下面是decode部份:
ByteBuffer[] inputBuffersAudio=null;
ByteBuffer[] outputBuffersAudio=null;
BufferInfo infoAudio=null;


if (audioTrack >=0)
{
 inputBuffersAudio = decoderAudio.getInputBuffers();
 outputBuffersAudio = decoderAudio.getOutputBuffers();
 infoAudio = new BufferInfo();
}
boolean isEOS = false;
long startMs = System.currentTimeMillis();
long lasAudioStartMs = System.currentTimeMillis();
while (!Thread.interrupted()) { 
 if (audioTrack >=0)
 { 
  if (!isEOS) {
   int inIndex=-1;
   try {
    inIndex = decoderAudio.dequeueInputBuffer(10000);
   } catch (Exception e) {
    e.printStackTrace();    
   }
   if (inIndex >= 0) {    
    ByteBuffer buffer = inputBuffersAudio[inIndex];
    int sampleSize = extractorAudio.readSampleData(buffer, 0);
    if (sampleSize < 0) {
     
     decoderAudio.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
     buffer.clear();
     isEOS = true;
    } else {     
     decoderAudio.queueInputBuffer(inIndex, 0, sampleSize, extractorAudio.getSampleTime(), 0);
     buffer.clear();
     extractorAudio.advance();
    }
    
   }
  }

  int outIndex=-1;
  try {
   outIndex = decoderAudio.dequeueOutputBuffer(infoAudio,10000);
  } catch (Exception e) {
   e.printStackTrace();   
  }

  switch (outIndex) {
  case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
   Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
   outputBuffersAudio = decoderAudio.getOutputBuffers();
   break;
  case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
   Log.d(TAG, "New format " + decoderAudio.getOutputFormat());
   playAudioTrack.setPlaybackRate(formatAudio.getInteger(MediaFormat.KEY_SAMPLE_RATE));
   break;
  case MediaCodec.INFO_TRY_AGAIN_LATER:
   Log.d(TAG, "dequeueOutputBuffer timed out!");
   break;
  default:
   if(outIndex>=0)
   {
    ByteBuffer buffer = outputBuffersAudio[outIndex];
    byte[] chunk = new byte[infoAudio.size];
    buffer.get(chunk);
    buffer.clear();
                if(chunk.length>0){         
                 playAudioTrack.write(chunk,0,chunk.length);
                }                
    decoderAudio.releaseOutputBuffer(outIndex, false);
   }
   break;
  }

  // All decoded frames have been rendered, we can stop playing now
  if ((infoAudio.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
   Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
   break;
  }
 }
}

if (audioTrack >=0)
{
 decoderAudio.stop();
 decoderAudio.release();
 playAudioTrack.stop();
}

extractorAudio.release();

和video差不多,就不解釋了,
唯一要注意的是decoderAudio.releaseOutputBuffer(outIndex, false)
後面的參數記得要設為false,也就是不需要render

[Android] MediaExtractor 和 MediaCodec 如何配合使用 - Video篇

MediaExtractor 是用來將來源的影音資料分開,
它支援HTTP streaming或者是local的檔案,
video和audio分開之後再丟給MediaCodec去decode,

首先看一下video部份,
video需要render,所以還另外需要加上SurfaceView
private MediaExtractor extractorVideo;
private MediaCodec decoderVideo;

extractorVideo = new MediaExtractor();
extractorVideo.setDataSource("myTest.mp4"); 

for (int i = 0; i < extractorVideo.getTrackCount(); i++) {
 MediaFormat format = extractorVideo.getTrackFormat(i);
 String mime = format.getString(MediaFormat.KEY_MIME);
 Log.d(TAG, "mime=>"+mime);
 if (mime.startsWith("video/")) {
  videoTrack = i;  
  extractorVideo.selectTrack(videoTrack);
  decoderVideo = MediaCodec.createDecoderByType(mime);
  decoderVideo.configure(format, surface, null, 0);
  break;
 }
}

if (videoTrack >=0) {
 if(decoderVideo == null)
 {
  Log.e(TAG, "Can't find video info!");
  return;
 }
 else
  decoderVideo.start();
}
extractorVideo 會根據MIME的資訊把video track找出來,
把video format和surface透過decoderVideo.configure()傳給decoderVideo,
之後decoderVideo便會接手decode和render的工作,

 接下來就是decode部份:
ByteBuffer[] inputBuffersVideo=null;
ByteBuffer[] outputBuffersVideo=null;
BufferInfo infoVideo=null;

if (videoTrack >=0)
{
 inputBuffersVideo = decoderVideo.getInputBuffers();
 outputBuffersVideo = decoderVideo.getOutputBuffers();
 infoVideo = new BufferInfo();
}

boolean isEOS = false;
long startMs = System.currentTimeMillis();

while (!Thread.interrupted()) {
 if (videoTrack >=0)
 {     
  if (!isEOS) {      
   int inIndex=-1;
   try {
    inIndex = decoderVideo.dequeueInputBuffer(10000);
   } catch (Exception e) {
    e.printStackTrace();    
   }

   if (inIndex >= 0) {
    ByteBuffer buffer = inputBuffersVideo[inIndex];
    int sampleSize = extractorVideo.readSampleData(buffer, 0);
    if (sampleSize < 0) {
     // We shouldn't stop the playback at this point, just pass the EOS
     // flag to decoder, we will get it again from the dequeueOutputBuffer     
     decoderVideo.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
     buffer.clear();
     isEOS = true;
    } else {     
     long current = System.currentTimeMillis();
     decoderVideo.queueInputBuffer(inIndex, 0, sampleSize, extractorVideo.getSampleTime(), 0);     
     buffer.clear();
     extractorVideo.advance();
    }
   }
  }
  int outIndex=-1;
  try {
   outIndex = decoderVideo.dequeueOutputBuffer(infoVideo,10000);
  } catch (Exception e) {
   e.printStackTrace();   
  }
  switch (outIndex) {
  case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
   Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
   outputBuffersVideo = decoderVideo.getOutputBuffers();
   break;
  case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
   Log.d(TAG, "New format " + decoderVideo.getOutputFormat());
   break;
  case MediaCodec.INFO_TRY_AGAIN_LATER:
   Log.d(TAG, "dequeueOutputBuffer timed out!");
   break;
  default:
   if(outIndex >=0)
   {
    ByteBuffer buffer = outputBuffersVideo[outIndex];    
    buffer.clear();
    decoderVideo.releaseOutputBuffer(outIndex, true);
    // We use a very simple clock to keep the video FPS, or the video
    // playback will be too fast
    while (infoVideo.presentationTimeUs / 1000 > (System.currentTimeMillis() - startMs)) {
     try {      
      sleep(10);
     } catch (InterruptedException e) {
      e.printStackTrace();
      Thread.currentThread().interrupt();
      break;
     }
    }
   }
   break;
  }

  // All decoded frames have been rendered, we can stop playing now
  if ((infoVideo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
   Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
   break;
  }
 }
}
if (videoTrack >=0)
{
 decoderVideo.stop();
 decoderVideo.release();
}

extractorVideo.release();

先呼叫decoderVideo.dequeueInputBuffer(10000)取得一個input buffer,
利用extractorVideo.readSampleData(buffer, 0)將資料放到buffer去,
然後再呼叫decoderVideo.queueInputBuffer()將資料queue到後面的decode工作, decoderVideo.dequeueOutputBuffer(infoVideo,10000)如果傳回非-1的值,
表示decode成功,
然後透過decoderVideo.releaseOutputBuffer(outIndex, true)將它render到surface,
這樣video的工作就完成了

2015年3月30日 星期一

[Corona SDK] graphics.newImageSheet 怎麼用?

如果我們要放上一張圖,
只要使用display.newImage()去載入圖檔即可,
圖檔很多時,程式就會顯得很繁瑣,

如果這些圖是有高度相關性,甚至大小也都相同,
那麼使用graphics.newImageSheet()會是一個更好的選擇,
假設我們要畫上撲克牌的52張牌,
我們先把所以牌放在同一張圖裡,如下:
card.png
程式代碼:
display.setDefault( "background", 0.2, 0.5, 0.2 )

local frames_width = 73
local frames_height = 98
local total_frames = 52
local image_name = "card.png"
local scaleRatio = 0.6
local options =
{
 width = frames_width,
 height = frames_height,
 numFrames = total_frames,
 --sheetContentWidth = 754,  -- width of original 1x size of entire sheet
    --sheetContentHeight = 311   -- height of original 1x size of entire sheet
}

local sheet = graphics.newImageSheet( image_name, options )
cardGroup = {}
for i=0,3,1 do
 for j=1,13,1 do
  cardGroup[i*13+j] = display.newImage( sheet,i*13+j)
  cardGroup[i*13+j]:scale( scaleRatio, scaleRatio )
 end
end
--------------------------------------------------------------------------------
for i=0,3,1 do
 for j=1,7,1 do 
 cardGroup[i*13+j]:translate( (frames_width*scaleRatio+2)*(j-1)+(frames_width*scaleRatio+2)/2,(frames_height*scaleRatio+2)/2+(i*2)*(frames_height*scaleRatio+2)) 
 end 
 for j=1,6,1 do 
 cardGroup[i*13+j+7]:translate( (frames_width*scaleRatio+2)*(j-1)+(frames_width*scaleRatio+2)/2,(frames_height*scaleRatio+2)/2+(i*2+1)*(frames_height*scaleRatio+2)) 
 end 
end
graphics.newImageSheet( image_name, options )只要傳進圖檔名稱和參數options即可,
options裡的sheetContentWidthsheetContentHeight是原始圖檔的大小,不用指定也可以,
options裡的widthheight是每張小圖的大小,
options裡的numFrames是圖檔裡小圖的數量,

然後在display.newImage()傳入剛剛建立的image sheet以及想要顯示第幾個小圖
執行結果如下:
View As 1080x920
如果每張小圖的大小不一樣,
那options裡就需要一張一張指定參數:
local options =
{
    --array of tables representing each frame (required)
    frames =
    {
        -- FRAME 1:
        {
            --all parameters below are required for each frame
            x = 2,
            y = 70,
            width = 50,
            height = 50
        },

        -- FRAME 2:
        {
            x = 2,
            y = 242,
            width = 50,
            height = 52
        },

        -- FRAME 3 and so on...
    },

}

2015年3月24日 星期二

[Corona SDK] else if 和 elseif 的差別?

if的基本用法
local i = 0
if(i==1) then
 print(1)  --> will not print
end
注意,if後面需要接著一個then
事實上,很多程式語言也是需要那個then
只是它們可以接受不寫,但在Lua一定要

多一個else呢:
local i = 0
if(i==1) then
 print(1)  --> will not print
else
 print(0)  --> will print
end

二層if呢:
local i = 0
if(i==1) then
 print(1)  --> will not print
elseif (i==0) then
 print(0)  --> will print
else
 print(123) --> will not print
end

elseif可以寫成 else if嗎?
local i = 0
if(i==1) then
 print(1)  --> will not print
else if (i==0) then
 print(0)  --> will print
else
 print(123) --> will not print
end
end
結果是一樣的,怎麼好像多了一個end而已,why??

再多一層:
local i = 0
if(i==1) then
 print(1)  --> will not print
elseif (i==2) then
 print(2)  --> will not print
elseif (i==0) then
 print(0)--> will print
else
 print(123) --> will not print
end

一樣把elseif 改成 else if看看:
local i = 0
if(i==1) then
 print(1)  --> will not print
else if (i==2) then
 print(2)  --> will not print
else if (i==0) then
 print(0)--> will print  
else
 print(123) --> will not print
end
end
end
哇,怎麼後面要3個end啊?
因為else表示是前面所有判斷式都不成立時就會進來它裡面,
不管前面有幾個elseif
elseif表示要先判斷成不成立再執行,
如果是else if,那表示已經進到else了,只是我們再多了一個if的去判斷,
將上面的程式增加二行:
local i = 0
if(i==1) then
 print(1)  --> will not print
else 
 print("enter 1") --> will print 
 if (i==2) then
 print(2)  --> will not print
else 
 print("enter 2") --> will print 
 if (i==0) then
 print(0)--> will print  
else
 print(123) --> will not print
end
end
end
如果還不夠清楚,程式不改,我們重新排版看看:
local i = 0
if(i==1) then
 print(1)  --> will not print
else 
 print("enter 1") --> will print 
 if (i==2) then
  print(2)  --> will not print
 else 
  print("enter 2") --> will print 
  if (i==0) then
   print(0)--> will print  
  else
   print(123) --> will not print
  end 
 end
end
所以,標準的用法就是
if(condition 1) then
 --do some things
elseif (condition 2) then
 --do some things
 .
 .
elseif (condition n) then
 --do some things
else
 --do some things
end

[Corona SDK] 再談Table

[Corona SDK] Table 的基本用法裡我們看到了table的用法,
事實上,Lua很多地方都有table的影子,
例如,我們提到t["name"]可以用t.name來存取,
耶?怎麼好像我們呼叫的Lua API也是長得很像,
沒錯,那些funtions就是用table把它們群組在一起,
例如display相關的API,就是有一個叫displaytable,
裡面有不同的index對應到不同的function,
所以...
display.newText( "ee", display.contentCenterX, 80, native.systemFont, 20 )
display["newText"]( "ee", display.contentCenterX, 80, native.systemFont, 20 )
上面兩種寫法都是可以的,
這個table還真是強大啊...

假如我們有很多功能相關的functions,
也是可以仿效這種作法,
用起來會和其它程式語言的method類似,

如果function只要傳參數進去,
那上面的方法很好用,
但如果是要對物件做操作呢?
例如,我們畫了一個矩形,並且對它做旋轉
local rec = display.newRect( 100, 100, 50, 50 )
rec.rotate(rec,45)
此時必須把要被旋轉的物件也傳進去,
通常是物件自己本身(當然也可以傳別的物件,不過這樣用法有點奇怪),
既然如此,Lua提供了一個稍微簡易的寫法
local rec = display.newRect( 100, 100, 50, 50 )
rec:rotate(45)
也就是用":"來取代".",此時就不用多傳一個物件參數

[Corona SDK] 運算式 Expression

數學運算式 Arithmatic Operators
大部份基本的運算都有: +(加), -(減), *(乘), /(除), %(求餘), ^(指數)
沒什麼特別的,直接看範例:
print(17+2)      --> 19
print(-17-2)     --> -19
print(17*5)      --> 85
print(17/3)      --> 5.6666666666667
print(17%5)      --> 2
print(17^2)      --> 289
print(17^(-0.5)) --> 0.24253562503633, 相當於開根號

關係運算式 Relational Operators
有幾下幾種可用:
==     ~=      <      <=      >       >=
==常常被用來檢查二變數或物件是否相同,
它會檢查型態,如果型態不同,直接回傳false,
如果型態相同,而來會比較值是否一樣,
換句話說,之前[Corona SDK] 基本型態和操作提到的stringnumber自動轉換在此不適用,
例如, 123 == "123"會回傳false,
另外tablefunction的比較則是用reference,不是用value,
例如以下範例,雖然兩個table內容一樣,但結果會是false
t1 = {my,"123"}
t2 = {my,"123"}
print(t1==t2)   --> false
另外,~=就是將==反過來

邏輯運算式 Logical Operators
or, 如果第一個參數不是false且不是nil, 即傳回第一個參數,不然就傳回第二參數,
and, 如果第一個參數是falsenil, 即傳回第一個參數,不然就傳回第二參數,
not, 回傳truefalse
print(1 or 2)            --> 1
print(nil or true)       --> true
print(false or nil)      --> nil
print(3 and 4)           --> 4
print(not nil)           --> true
print(nil and "test")    --> nil
print(false and nil)     --> false
print(1 or 2 and 3)      --> 1
print(nil or 2 and 3)    --> 3
print(nil and 2 or 3)    --> 3

字串相加 Concatenation
用".."即可將兩字串相加,
之前[Corona SDK] 基本型態和操作提到的stringnumber自動轉換在此也適用
print("a" .. "b")       --> ab
print("a" .. 2)         --> a2
print(1 .. 2)           --> 12

長度 Length Operator
在變數前面加#即可取得stringtable的長度資訊
a = "hello"
print(#a)       --> 5
b = "哈囉你好"
print(#b)       --> 12
c = {}
c[1] = "33"
c["a"] = "55"
c[2] = "33"
print(#c)       --> 2
d = {}
d[0] = "33"
d["a"] = "55"
d[1] = "33"
d[3] = "33"
print(#d)       --> 1
e = {}
e[2] = "33"
e["a"] = "55"
e[3] = "33"
print(#e)       --> 0
中文字每個字有3個bytes,
table型態的長度是取連續數字index的最後一個為長度,
但如果index 1的項目為nil,則長度為0
例如 c[1],c[2]的index連續,且c[1]不是nil,所以長度為最後一個index值2,
d[0],d[1],d[3]的index不連續,所以長度為連續的最後一個index值1,
e[2],e[3]雖然連續,但是e[1]是nil,所以長度是0

2015年3月23日 星期一

[Corona SDK] Table 的基本用法

Lua的Table很特別,
它可以接受任何型態的資料,除了nil
而且這些型態是混放在同一個table裡,

它的index,也不限於用數字,
也是可以接受任何型態的資料,除了nil

要建一個table,需先用"{}"建立它,
用下面的例子做說明
display.setStatusBar( display.HiddenStatusBar )
t = {} --create a table
t = {he = "today"} --create a table with single property "he"
t[1] = 123
t[5] = "this is 5"
t[true] = 789
t["my"] = 456
t["you"] = "this is you"
display.newText( t[1], display.contentCenterX, 80, native.systemFont, 20 )
display.newText( t[5], display.contentCenterX, 100, native.systemFont, 20 )
display.newText( t["my"], display.contentCenterX, 120, native.systemFont, 20 )
display.newText( t["he"], display.contentCenterX, 140, native.systemFont, 20 )
display.newText( t.you, display.contentCenterX, 160, native.systemFont, 20 )
if(t[true] == 789) then
 display.newText( "this is 789", display.contentCenterX, 180, native.systemFont, 20 )
else
 display.newText( "this is NOT 789", display.contentCenterX, 180, native.systemFont, 20 )
end
要存取時,我們會用t[]的語法,
有一個比較特別的是,如果index是用字串的話,
那除了用t["name"]的方式,也可以用t.name
例如上面的13行的t.you,
不過,t["name"]的"name"如果是數字開頭,
例如t["5r"],那就不能用t.5r來存取,
需注意的是t[1]不能寫成t.1,t.1表示是t["1"],中間的差異要注意
上面的執行結果如下:

[Corona SDK] 基本型態和操作

Corona SDK是採用一種叫Lua的程式語言,
Lua的型態定義是動態的,
它不像一般其它程式語言需要用類似int,String, boolean等去定義變數,
它會根據給定的參數型態自動定義,
例如:
t = 1  --自動設定為數值
t = "hello" --自動設定為字串
那可以先t = 1之後t = "hello"嗎?
也就是說同一個變數,但型態改變?
答案是可以的,
Lua語言很...隨和^^

它會有幾種基本型態:
nil:當你只有宣告變數而沒有給定後面的參數時,它就是nil,和一般語言的null類似,
例如:local t 或者 local t  = nil 都是,
此時如果對這個變數做操作會有錯誤
local t
display.newText( t, display.contentCenterX, display.contentWidth / 4, native.systemFont, 40 )
執行上面的代碼會得到"got nil"的錯誤訊息
boolean: 就是falsetrue,在條件判斷式中我們會用到,
如果給的參數是falsenil,那結果就是false,
其它種類都視為true,
if(true) then --條件會成立
display.newText( "hello true", display.contentCenterX, 20, native.systemFont, 20 )
end
if(false) then --條件不會成立
display.newText( "hello false", display.contentCenterX, 40, native.systemFont, 20 )
end
if(nil) then --條件不會成立
display.newText( "hello nil", display.contentCenterX, 60, native.systemFont, 20 )
end
if(1) then --條件會成立
display.newText( "hello 1", display.contentCenterX, 80, native.systemFont, 20 )
end
if(0) then --條件會成立
display.newText( "hello 0", display.contentCenterX, 100, native.systemFont, 20 )
end
在上面代碼裡,if(0)還是視為true,這和其它語言不同
number: 用來表示實數(double-precision floating-point) ,
例如以下範例都可以
12   12.0   12.1895   12189.5e-3   0.121895E2    0xA3   0Xb22f
e或E用來表示指數,0x或0X表示16進位數值
string: 用來表示字元陣列,最後面內含0表示結束
function:
table: 算是Lua基本的資料結構,[Corona SDK] Table 的用法

另外,numberstring的轉換是很特別的,
如果對一個number變數做字串操作,那它會先變成string型態,
例如以下代碼,雖然display.newText()是接受string來顯示,
但我們一樣可以丟給它number
t = 123
display.newText( t, display.contentCenterX, 100, native.systemFont, 20 )
如果對一個string變數做數學運算,那它會先變成number型態,
例如以下範例,我們也可以對於一個字串"456.2"做數學運算,最後結果會印出"457.2"
t = "456.2"
t = t + 1
display.newText( t, display.contentCenterX, 100, native.systemFont, 20 )
上面的變數t,只要原來字串可以轉換成number型態的都行,
例如:t = "0x3a"   t = "1E3"  都可以,

[Corona SDK] 如何加上註解

在C或Java裡,
如果我們要加上註解,
只要在前面加上"//"雙斜線即可,
那在Corona SDK裡呢?
很簡單,只要加上"--"雙減號即可,
那...如果是要將一個段落都變成註解呢?
只要用"--[["和"--]]"包起來即可,如:
--[[
print( 10 ) -- no action (comment)
--]]
print(10)雖然前面沒有"--",
但因為它在"--[["和"--]]"裡,
所以一樣沒有作用,算是註解的一部份,

那...如果我想將剛剛註解的段落變成有效呢?
就是取消註解的意思,
一個作法當然是將"--[["和"--]]"拿掉,
另一個簡單的作法,
尤其是你在開發階段,常常需要做測試時,
那就是在前面在加上"-"減號即可,如下:
---[[
print( 10 ) -- will print
--]]
上面的作法可立刻將來註解的段落變成有效的段落,
反過來,如果要再變註解的段落,把"-"減號拿掉即可,
如果你是用Sublime Text,而且有安裝Corona Editor,
[Corona SDK] 好用的編輯器 - Sublime Text 2裡所提的,
那麼,你在上面測試註解comment的功能時,
你會發現那些註解部份的顏色會跟著改變

[Corona SDK] 好用的編輯器 - Sublime Text 2

Corona 沒有特定的編輯器,
你可以用記事本,UltraEdit或者官方推薦的Sublime Text,
今天就來介紹一下Sublime Text如何安裝,

步驟1:
請到Sublime Text去下載最新版本,
目前預設是Sublime Text 2,
當然,你也可以選擇Sublime Text 3來下載,
安裝好之後,打開Sublime Text

步驟2:
在安裝Corona套件之前,我們要先讓Sublime Text有支援套件安裝的功能,
請同時按下ctrl鍵和`鍵,後面的這個`鍵,就是鍵盤上數字1左邊那個鍵,
或者,你也可以從選單"View"/"Show Console"叫出console畫面來,

接著在console裡貼上下面的整段文字
import urllib2,os,hashlib; h = 'eb2297e1a458f27d836c04bb0cbaf282' + 'd0e7a3098092775ccb37ca9d6b2e4b7d'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); os.makedirs( ipp ) if not os.path.exists(ipp) else None; urllib2.install_opener( urllib2.build_opener( urllib2.ProxyHandler()) ); by = urllib2.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); open( os.path.join( ipp, pf), 'wb' ).write(by) if dh == h else None; print('Error validating download (got %s instead of %s), please try manual install' % (dh, h) if dh != h else 'Please restart Sublime Text to finish installation')
上面是從網址https://packagecontrol.io/installation#st2複製而來,
如果版本不是2.0.2,那建議到網址再複製一次,
另外,Sublime Text 3請到網址https://packagecontrol.io/installation#st3去複製,
貼上後按Enter讓它去執行,然後重開Sublime Text
如果重開Sublime Text後,有遇到它建議你再重開,就再重開

步驟3:
同時按下"Ctrl"+"Shift"+"P"三鍵,或者從選單"Tools/Command Palette"叫出Command palette的畫面
輸入"Package Control: Install Package"後按下Enter,
幾秒後會跑出可安裝的packages列表,
此時輸入"Corona Editor"
安裝它

步驟4:
如果安裝成功,你會發現選單最右邊多了一個"Corona SDK"選項
我們到"Snippets"下任選一個功能看看,
例如"Snippets/Display/Image",它會自動加入
display.newImage( [parent,], filename [,baseDir] [,x,y] [,isFullResolution] )
在程式裡

步驟5:
打開任何一個".lua"檔案看看,
然後到選單"Preferences/Color Scheme"裡選擇你要的編輯畫面文字配色

步驟6:
在打開".lua"檔案的情形下,
按"Ctrl"+"B"或者執行選單"Tools/Build",
此時就會呼叫"Corona Simulator",執行那個lua的結果,
在"Corona Simulator"畫面,如果想回到Sublime Text,
可以按下"Ctrl"+"Shift"+"B"或執行選單"File/Open in Editor"即可

2015年3月19日 星期四

[Corona SDK] display.contentWidth 和 display.viewableContentWidth 的差別是什麼?

我們常常會用到 display.contentWidthdisplay.contentHeight
不管你是要放圖或顯示文字等等,
那...display.viewableContentWidthdisplay.viewableContentHeight又是幹嘛用的?
怎麼感覺這個好像比較是我們該用的參數?

如果你查了一下display.viewableContentWidth

官網的解釋是:
A read-only property that contains the width of the viewable screen area in content coordinates.
看完了更加覺得它比display.contentWidth更適合我們在程式裡使用,
真的是這樣嗎?

因為config.lua裡的scale有四種選擇,
只有在scale = "zoomEven"時,它們才有差異,
所以,我們就設定scale = "zoomEven"
來看看它們有何不同...

我們將main.lua的程式改寫如下:
display.setStatusBar( display.HiddenStatusBar )

local xpos = display.contentWidth/2
local ypos = display.contentHeight/2
local background = display.newImage( "320x480.jpg", xpos, ypos )
local xpos = display.contentWidth
local ypos = display.contentHeight
local myText = display.newText( xpos .. "x" .. ypos, display.contentWidth/2, display.contentHeight/2-20, native.systemFont, 40 )
local xpos = display.viewableContentWidth
local ypos = display.viewableContentHeight
local myText = display.newText(  xpos .. "x" .. ypos, display.viewableContentWidth/2, display.viewableContentHeight/2+20, native.systemFont, 40 )
myText:setFillColor( 1, 110/255, 110/255 )
在模擬器跑跑看
View As 640 x 960
在View As "320x480"或"640x960"時,
二者值是一樣的,
在來試試其它解析度看看,例如1080x1920.
View As 1080 x 1920
數值不一樣了,
而且,文字的位置也相對移動了,
如果你仔細研讀的話,你會發現似曾相識,
用個圖來說明原因,

原來,display.ContentWidth=320是相當於一開始可用的畫布寛度(A點到F點),
display.viewableContentWidth=270是相當於要scale時會截取的有效畫布寛度(B點到E點),
截取的中心點還是160,然後往兩旁展開,
如果程式裡用display.ContentWidth ,  中心點是160(C點),
如果程式裡用display.viewableContentWidth ,中心點就會變成是135(G點),
display.newText()的動作會先發生,然後才scale,
在scale到真的解析度寬度1080後,結果就會和上圖的情形一樣,

如果是1920x1080呢?
Viewa As 1920 x 1080
上面的圖,程式其實有稍微修改,
不然,文字會跑出視窗外,看不到了
local xpos = display.contentWidth/2
local ypos = display.contentHeight/2
local background = display.newImage( "320x480.jpg", xpos, ypos )
local xpos = display.contentWidth
local ypos = display.contentHeight
local myText = display.newText( xpos .. "x" .. ypos, display.contentWidth/2, display.contentHeight/2-20, native.systemFont, 40 )
local xpos = display.viewableContentWidth
local ypos = display.viewableContentHeight
local myText = display.newText(  xpos .. "x" .. ypos, display.contentWidth/2, display.contentHeight/2+20, native.systemFont, 40 )
myText:setFillColor( 1, 110/255, 110/255 )
從上面的解說,你會發現如果不是"zoomEven" Mode,
那用一個都可以,
如果是"zoomEven" Mode,
那就是要display.contentWidthdisplay.contentHeight
那...display.viewableContentWidthdisplay.viewableContentHeight要幹嘛?
它們可以讓你知道實際會出現的"有效"寬度和長度資訊,
就看你程式是不是有這樣的需求...

Note:理論上,在letterbox模式時,display.viewableContentWidthdisplay.viewableContentHeight應要比display.contentWidthdisplay.contentHeight來的大,
所以你會看到在letterbox mode時,上下或兩旁可能有黑邊現象,
display.viewableContentWidthdisplay.viewableContentHeight看起來是指"有效的"寬度和長度資訊,而不是"可視的"寬度和長度資訊,所以它最大值會受限於display.contentWidthdisplay.contentHeight,
letterbox mode,如果要取得包括黑邊的長度或寬度,應該是要用display.actualContentWidthdisplay.actualContentHeight
這二個參數在zoomEven mode也適用,它們的值會和display.viewableContentWidthdisplay.viewableContentHeight一樣

[Corona SDK] 為什麼 display.newImage() 的位置怪怪的? - anchor的用法

如果我們想要在畫面放上一張圖片:
local background = display.newImage( "320x480.jpg",0,0)
很簡單,只要一行,
用模擬器看一下...
anchorX=0.5, anchorY=0.5
耶?奇怪了,怎麼照片位置好像不太對,
原來,每個物件都有所謂的anchorX和anchorY,
anchor就是物件做任何操作的中心點,
不管你是移動,放大縮小,旋轉等等
上面的圖是從coronalabs官網的說明抓下來的,
anchorX/anchorY是從物件的左上角(0, 0)到右下角(1, 1),
預設值是(0.5, 0.5),也就是物件的中心點,
(anchor的數字是和物件大小相比的比例值)
也就是說,display.newImage( "320x480.jpg",0,0)是把圖片的中心點放到(0, 0)的位置,
當然就會看到第一張圖的樣子,
那要怎麼調回來讓圖位置正確呢?



local background = display.newImage( "320x480.jpg",0,0)
background.anchorX=0
background.anchorY=0
只要將anchor調到0就可以了
anchorX = 0, anchorY = 0
不過,你會發現demo的程式裡,好像都不是這樣做的,
沒錯,通常我們不會去改anchor預設值,除非有需要,
因為位置是相對的,
所以你可以用以下的做法:
local xpos = display.contentCenterX
local ypos = display.contentCenterY
local background = display.newImage( "320x480.jpg", xpos, ypos )
或者有人是這樣用
local xpos = display.contentWidth/2
local ypos = display.contentHeight/2
local background = display.newImage( "320x480.jpg", xpos, ypos )
意思都是一樣,因為anchor預設是在圖片中央, 將圖片中央放在畫面中央,當然就會是對的...

2015年3月18日 星期三

[Android] 如何藉由JNI來呼叫底層Native的C/C++程式 - part I

基本上,Android提供的API已足夠一般寫程式的需求,
不過,有時候我們仍然會需要直接呼叫底層Native的C/C++程式,
有時是因為效能,有時是因為Android提供的功能不足...

如果你也是使用Eclipse,
那麼,你必須先安裝CDT和NDK,
之後,在你想要增加JNI介面的project按右鍵,
執行"Android Tools"/"Add Native Support"後,
你會發現project下面多了一個"jni"目錄,
目錄底下還有一個"x.cpp"和"Android.mk",
到這邊,你已經完全了第一步了...

接下來,Java要如何呼JNI的函式呢?

package myTest.com;
 
import android.app.Activity;
import android.os.Bundle;
 
public class MainActivity extends Activity { 
 private native String hello(String s); 
 
 static {
  System.loadLibrary("NativeMyTest");
 } 
}
"NativeMyTest"是你剛剛產生jni目錄時,輸入的名稱,
也就是說,待會會產生NativeMyTest.so檔,
 hello(String s)則是定義在JNI裡的函式,
 那JNI這邊呢?
#include <jni.h>
#include <stdio.h>

 #define LOG_TAG "MainActivity"
#define LOGI(...) __android_log_print(4, LOG_TAG, __VA_ARGS__);

JNIEXPORT void hello(JNIEnv* env, jobject obj, jstring str){ 
 LOGI("hello: %s", str);
}

jint JNI_OnLoad(JavaVM* pVm, void* reserved) {
  JNIEnv* env;
  if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
  return -1;
  }
 
  JNINativeMethod nm[2];
  nm[0].name = "hello";
  nm[0].signature = "(Ljava/lang/String)V";
  nm[0].fnPtr = (void*)hello;

  jclass cls = (*env)->FindClass(env, "myTest/com/MainActivity");
  (*env)->RegisterNatives(env, cls, nm, 1);
  gJavaVM = pVm;
  return JNI_VERSION_1_6;
}
JNI_OnLoad()是用來載入那些會被Java程式所呼叫的函式,
FindClass()傳入的參數必須和Java程式的package和class名稱一樣,
執行時,看看是載入NativeMyTest.so有問題,還是沒有發現hello函式,
再來決定是那裡有問題...

[Corona SDK] config.lua中的scale - 實例演練

[Corona SDK] config.lua中的scale怎麼設定呢?裡,
我們看到了config.lua中scale的意義,
現在就來實際演練一下,
實際操作時,不只width,height,scale要注意,
原圖的大小也有關係,
Simulator裡"View As"所選的手機解析度也會影響,
假設原圖是一張320x480大小:

1. 如果我們config.lua的設定如下:

width = 320,
height = 480,
scale = "none"
View As 320 x 480
View As 640 x 960
View As 1080 x 1920
View As 1920 x 1080
那麼,因為圖和原始畫布比例一樣,
所以不管用什麼解析度的手機來看都是完整的,雖然有的可能會變形

2. 如果將scale改成letterbox呢?

width = 320,
height = 480,
scale = "letterbox"
View As 320 x 480
View As 640 x 960
View As 1080 x 1920
View As 1920 x 1080
在320x480以及640x960的解析度,畫面都是滿框不變形,因為它們比例一樣,
在1080x1920的解析度,上下有黑邊,因為它的長寛比和320x480不同,所以scale後,相當於320x480的圖畫在320x568左右的畫布上,
在1920x1080的解析度,左右有黑邊,因為它的長寛比和320x480不同,所以scale後,相當於320x480的圖畫在853x480左右的畫布上,

3. 如果將scale改成zoomEven呢?
width = 320,
height = 480,
scale = "zoomEven"
View As 320 x 480
View As 640 x 960
View As 1080 x 1920
View As 1920 x 1080


一樣,320x480以及640x960的解析度,畫面都是滿框不變形,因為它們比例一樣,
在1080x1920的解析度,左右被截掉了,因為它的長寛比和320x480不同,所以scale後,相當於320x480的圖畫在270x480左右的畫布上,
在1920x1080的解析度,上下被截掉了,因為它的長寛比和320x480不同,所以scale後,相當於320x480的圖畫在320x180左右的畫布上,

4. 如果將scale改成zoomStretch呢?
width = 320,
height = 480,
scale = "zoomStretch"
View As 320 x 480
View As 640 x 960
View As 1080 x 1920
View As 1920 x 1080
在320x480以及640x960的解析度,畫面都是滿框不變形,因為它們比例一樣,
在1080x1920和1920x1080的解析度,畫面是滿框,但會變形

如果原圖不是320x480呢?
也就是說,如果原圖長寬比和config.lua裡的長寬比不同呢?
那就必須同時考慮原圖,config.luawidth/height設定,再配合scale的參數,觀念是類似的...
基本上,如果你的原圖只是當背景用,比例不重要的話,那scale就設成zoomStretch
如果比例很重要,但是可以接受截圖,那scale就設成zoomEven
如果比例很重要,但是可以接受黑邊,那scale就設成letterbox
如果比例很重要,不能接受截圖和黑邊,那...你就必須準備多張不同解析度的圖囉

[Corona SDK] config.lua中的scale怎麼設定呢?

各家手機解析度都不同,
為了解決這個問題,
Corona SDK利用config.lua來設定,
裡面的widthheight,你可以想成是初始的畫布大小,
最終的畫布大小和你所用的手機解析度來決定,
scale呢?初始的畫布長寬比和手機解析度長寬比如果不同時,
怎麼處理放大縮小的原則就是由這個參數決定,
有幾種模式可選:
"none":就是不作用
"letterbox":會盡量將所有內容全部在畫面中顯現,而且會維持原來的比例,但因為這樣,所以有時會有黑邊出現
letterbox

"zoomEven":會將內容全部塞滿畫面,而且會維持原來的比例,換句話說,內容可能會跑出面,也就是被截掉了
zoomEven

"zoomStretch":會將內容全部塞滿畫面,但不會維持原來的比例,換句話說,內容可能會變形
zoomStretch