2015年12月13日 星期日

jQuery之Deffered的應用

jQuery有Deferred及Promise物件,可以幫助我們處理異步函式的callback順序關係。
在這邊舉兩個例子來說明其可能的兩個應用:

一、用Deferred實現異步函式的callback順序關係
Deferred可以實現異步函式的callback順序關係,在下例中,我們有三個函式,f1()、f2()、f3(),其中都有setTimeout模擬一秒的異步延遲,並且希望能f1做完才執行f2、f2做完才執行f3。如果我們沒有處理好callback的順序關係的話,就會發生f1的setTimeout還沒執行完就執行f2的情況。

在此例中,我們利用Deferred.then()來確保f1,f2,f3的執行順序關係,在f1,f2,f3中,會個自建立一個Deferred物件並傳回。
而setTimeout結束後,會呼叫Deferred.resolve()來告知函式執行完成,並可以選擇送進一個參數(param)來給之後的callback函式使用,接著會執行then()裡面的函式。

以上說明是簡單的行為了解。其實真正的實作比較複雜,可以參考jQuery之Deferred的原理,在這邊稍微講一下大概的背後過程:

  1. f1()回傳了第一個Deferred物件。
  2. f1('f1').then(f2)建立了第二個Deferred物件,並為第一個Deferred物件設定done(ff2),ff2是callback函式,ff2的內容是執行f2(),並取得f2()回傳的第三個Deferred物件,接著設定第三個物件的done(fff2),其中fff2=第二個Deferred物件.resolve。
  3. 所以當呼叫then(f2)時,會回傳一個Deferred物件(即第二個Deferred物件。
  4. 當f1()裡呼叫defer.resolve()時,會執行f2()。
  5. 當f2()裡呼叫defer.resolve()時,會引發第三個Deferred物件的done(),接著就會引發第二個Deferred物件的resolve()。
  6. 以此類推,第二個Deferred物件被resolve()後就會去執行f3()。


HTML:
<div id='result'>
  Result:
</div>
Javascript:
function f1(param) {
  var defer = $.Deferred();
  setTimeout(function() {
    $('#result').html($('#result').html() + '<br/>f1 got parameter : ' + param);
    //將'f2'當參數傳給don()或then()之後的function
    defer.resolve('f2');
  }, 1000);
  return defer;
}

function f2(param) {
  var defer = $.Deferred();
  setTimeout(function() {
    $('#result').html($('#result').html() + '<br/>f2 got parameter : ' + param);
    //將'f3'當參數傳給don()或then()之後的function
    defer.resolve('f3');
  }, 1000);
  return defer;
}

function f3(param) {
  var defer = $.Deferred();
  setTimeout(function() {
    $('#result').html($('#result').html() + '<br/>f3 got parameter : ' + param);
    //defer.resolve(param + ' f3');
  }, 1000);
  return defer;
}
//Deferred參數的傳遞
f1('f1').then(f2).then(f3);
二、合併多個異步函式的callback處理,即個別異步函式都執行完後才執行callback

在此例中我們用到了$.when(),它可以被傳入多個Deferred或Promise物件,並在所有的Deferred或Promise物件都被resolve()或reject()後才執行callback。

在這裡我們有三個動畫,都為一串文字由右往左移動,每個動畫的時間都不一樣,第一個是用CSS的動畫,其他兩個是用jQuery的animate()做的動畫。

對於每個動畫,我們都建立一個新的Deferred物件給它,並在動畫執行完後將它得到的Deferred給resolve()。而每個Deferred物件我們都放進一個叫des的Array中。

最後我們把Array裡面的三個Deferred物件丟給$.when()裡當參數,利用then()或done()來設定三個Deferred物件都resolve()後才要執行的callback。

P.S.

  1. 因為$.when()不能接受Array型式的參數,所以這裡利用了apply($,des)來將des中的內容傳給when(),其中第一個參數為要代替when()中的this的參考,第二個參數是一個Array,其內容為要傳給when()的參數。
  2. resolveDeferred()接受一個Deferred物件,並傳回一個function,在傳回的function中其this指向呼叫resolveDeferred的物件;而deferred指向function被建立的域(即resolveDeferred())中的deferred。(可參見閉包(Closure))


HTML:
<div class='animSection'>
  <div id='anim1'>Watch me move1</div>
  <div id='anim2'>Watch me move2</div>
  <div id='anim3'>Watch me move3</div>
</div>
<br/>
<div id='text' class="text">Detect Result:</div>
CSS:
.animSection div{
  padding-left: 100%;
}

.animClass {
  animation-name: myAnim;
  animation-duration: 6s;
  animation-fill-mode: forwards;
}

@keyframes myAnim {
  0% {
    padding-left: 100%;
  }
  100% {
    padding-left: 0%;
  }
}
Javascript:
var des = []; //用來放Deferred或Promise物件的array
var deferred;

deferred = $.Deferred();
des.push(deferred.promise()); 
//也可以寫成des.push(deferred),deferred.promise()回傳的為Promise物件,為不可修改狀態只供查詢狀態的物件,即沒有resolve()等方法
//
$('#anim1').addClass('animClass').one('animationend', resolveDeferred(deferred));

deferred = $.Deferred();
des.push(deferred.promise());
$('#anim2').animate({
  paddingLeft: "-=100%"
}, 9000, resolveDeferred(deferred));

deferred = $.Deferred();
des.push(deferred.promise());
$('#anim3').animate({
  paddingLeft: "-=100%"
}, 5000, resolveDeferred(deferred));
//因為$.when()只能接收一個一個用逗號分開的參數,所以利用apply來將array以個別
//參數的方式傳給when(),when()可接受Deferred及Promise物件
$.when.apply($, des).done(function() {
  $('#text').html($('#text').html() + '<br/>All finished!');
});
//也可以寫成以下,then可以給兩個參數,第一個為done()的callback函式,
//第二個為fail()的callback函式
//$.when.apply($, des).then(function(){
//	$('#text').html($('#text').html() + '<br/>All finished!');
//});

function resolveDeferred(deferred) {
  return function() {
    $('#text').html($('#text').html() + '<br/>#' + this.id + ' finished!');
    deferred.resolve();
  }
}

2015年12月12日 星期六

jQuery之Deferred的原理(參看源碼)

了解了jQuery的Callbacks以後,我們可以藉由觀看jQuery的源始碼(jquery-1.11.3.js)來較清楚地了解Deferred到底做了什麼事及怎麼實現的,以更好的運用Deferred及其相關API。

首先,我們先把Deferred的jQuery源碼貼出來:

 Deferred: function( func ) {
  var tuples = [
    // action, add listener, listener list, final state
    [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
    [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
    [ "notify", "progress", jQuery.Callbacks("memory") ]
   ],
   state = "pending",
   promise = {
    state: function() {
     return state;
    },
    always: function() {
     deferred.done( arguments ).fail( arguments );
     return this;
    },
    then: function( /* fnDone, fnFail, fnProgress */ ) {
     var fns = arguments;
     return jQuery.Deferred(function( newDefer ) {
      jQuery.each( tuples, function( i, tuple ) {
       var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
       // deferred[ done | fail | progress ] for forwarding actions to newDefer
       deferred[ tuple[1] ](function() {
        var returned = fn && fn.apply( this, arguments );
        if ( returned && jQuery.isFunction( returned.promise ) ) {
         returned.promise()
          .done( newDefer.resolve )
          .fail( newDefer.reject )
          .progress( newDefer.notify );
        } else {
         newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
        }
       });
      });
      fns = null;
     }).promise();
    },
    // Get a promise for this deferred
    // If obj is provided, the promise aspect is added to the object
    promise: function( obj ) {
     return obj != null ? jQuery.extend( obj, promise ) : promise;
    }
   },
   deferred = {};

  // Keep pipe for back-compat
  promise.pipe = promise.then;

  // Add list-specific methods
  jQuery.each( tuples, function( i, tuple ) {
   var list = tuple[ 2 ],
    stateString = tuple[ 3 ];

   // promise[ done | fail | progress ] = list.add
   promise[ tuple[1] ] = list.add;

   // Handle state
   if ( stateString ) {
    list.add(function() {
     // state = [ resolved | rejected ]
     state = stateString;

    // [ reject_list | resolve_list ].disable; progress_list.lock
    }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
   }

   // deferred[ resolve | reject | notify ]
   deferred[ tuple[0] ] = function() {
    deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
    return this;
   };
   deferred[ tuple[0] + "With" ] = list.fireWith;
  });

  // Make the deferred a promise
  promise.promise( deferred );

  // Call given func if any
  if ( func ) {
   func.call( deferred, deferred );
  }

  // All done!
  return deferred;
 }

再來一步步解析:

jQuery的Callbacks

jQuery有一個對控制異步函式時很好用的類別Deferred,還有它的簡少API版本,Promise(無法更改state)。

我們可以藉由觀看jQuery的源始碼(jquery-1.11.3.js)來較清楚地了解Deferred到底做了什麼事及怎麼實現的,以更好的運用Deferred及其相關API。

在了解Deferred之前,我們可以先來了解Callbacks這個類別。在這邊用形象的方式來了解Callbacks的運作方式。

Callbacks可以被當成是一個佇列式的列隊(先進先出,FIFO),我們可以把想要做的function傳進列隊中(add()),並在想要的時候觸發列隊中的function執行(fire() or fireWith()),在這邊是一次執行所有的function,雖然有照順序開始執行,但並無法保證一個function執行完才執行下一個(例如要花時間的異步函式)。

而Deferred則是利用Callbacks的性質來實作的,基本上就是預先將異步函式做完時要執行的回調函式先放到列隊中,並執行異步函式,等異步函式執行完後手動決定要觸發列隊裡函式並執行的時機。

jQuery的Callbacks有以下常用的API function
  1. $.Callbacks([flags]) : 創建並返回一個Callbacks類別。
    • flags : 可選,可以接受多個,用空格的String來表示,例如有以下幾種:
      •  once:
        • 列隊只能被觸發一次(fire)。
      •  memory:
        • 列隊觸發後,一有函式放進(add)列隊會被馬上執行。
      •  unique:
        • 列隊中的同樣函式(指的是其參考一樣的 reference)再一次觸發時只會被執行一次。
  2. Callbacks.add(callbacks) : 將callbacks函式放進Callbacks列隊中。
  3. Callbacks.fire([arguments]) : 觸發列隊執行其中的函式。
    • arguments : 可選,可傳入參數給列隊中各函式當input value。
  4. Callbacks.fireWith([context] [, arguments]) : 跟fire()相似,也是觸發列隊執行其中的函式,不過可以指定上下文(context),即函式中的this指誰。
  5. Callbacks.disable() : 將callback列隊disable掉,即之後不能在做fire()之類的呼叫(呼叫了也不會有行為)
範例 :

2015年12月5日 星期六

CSS與jQuery的動畫(animation)比較

jQuery與CSS都能實現對網頁上的DOM進行動畫特效,但兩者有一些差異,特別在這邊紀錄:

jQuery的動畫:

  1. 可使用$(selector).animate(styles,speed,easing,callback)來進行動畫,為有數字參數的css屬性進行動畫(無法用於只有字串值的css屬性,例如background-color:red)。
  2. 可使用$(selector).stop(stopAll,goToEnd)來停止動畫,可以只停止當前動畫並繼續 queue中的其他動畫。也可以停止當前動畫並情空動畫列隊(只限queue中的animation列隊成員)(參見queue),此時可以選擇直接將元素(DOM element)的動畫效果跳至當前動畫的最後狀態。
  3. 沒有暫停的方法(pause),即無法中途暫停,之後再從暫停的地方播放剩餘的動畫(resume)。目前要有pasue、resume的方法只能用plug-in等的方式解決,這裡有一些還沒試過的plug-in可以參考: PausejQuery-FxQueues
  4. 動畫結束後並不會引發animation事件。
CSS的動畫
  1. 為DOM element加上附有動畫屬性的CSS來實現動畫。
  2. 在CSS中使用animation-name屬性來指定keyframes (參見CSS3 Animations)。故無法同時在同一個DOM element上指定兩個animation-name的CSS屬性,會被最後決定的屬性覆蓋過去,即一次只能一個動畫。
  3. 可以設定許多不同的屬性來達到不同的效果。例如用animation-iteration-count來設定重覆次數
  4. 可以使用animation-play-state來設定運行(running)、暫停(paused),pased及running可以互相切換,動畫不會被停止無法繼續播(即paused之後再running等同resume效果)。
  5. 動畫播放完以後nimation-play-state會被設為paused。
  6. 動畫無法停止
  7. animation-timing-function可以設定時間與output效果的關係(參見<timing-function>),常用的有可以用來做逐幀動畫的steps(number_of_steps, direction),作用為設定output效果為步進函數(step function),number_of_steps代表切割動畫(動畫指瀏覽器算出來的漸進動畫output效果)為幾幀,direction代表步進的不連續點為每幀的start處還是end處(參見<timing-function>CSS3 timing-function: steps() 詳解小tip: CSS3 animation漸進實現點點點等待提示效果)。
  8. 動畫結束後發出animationend事件,可用JavaScript的addEventListener('animationend',callbackFunction)或jQuery的bind('animationend',callbackFuncion)來獲取事件,但是如果動畫class被移除或animation-play-state被設成paused並不會引發animationend事件。

JavaScript, jQuery 的Event機制 (Capturing, Bubbling)

在Javascript的W3C標準中,當Evnet(事件)發生時,例如click事件、animationEnd事件、hover事件等,Event會以由外到內(Capturing)、再由內到外(Bubbling)的方式傳遞。

我們可以用JavaScript的方法,addEventListener(eventName , callbackFunction , capturOrBubble)
來設置Evnet的監聽器,其中

第一個參數eventName為一個String,表示事件名稱,跟jQuery的bind()及one()不同,不可用空白分隔多個事件。

第二個參數是一個callback方法,可以選擇傳入event參數來得到event資訊,例如可以用event.target或event.srcElement (IE適用) 來得到Event作用的JavaScript對像。

我們也可以用Jquery的方法,bind(eventName,callbackFunction),來做到一樣的事情,不過要要注意bind()的callbackFunction傳入的event不是JavaScript的Event物件,而是被包裹成jQuery版的Event物件,雖然一樣有event.target,但是確沒有event.srcElement,如果要拿到JavaScript版的Event物件,可以使用event.origianlEvent來得到。

現在用下面這個例子來說明:

html:

<div id='1'>
  <div id='2'>
    <div id='3' class='animClass'>Watch me move</div>
  </div>
</div>
<div id='text'></div>


CSS :
.animClass {
  animation-name: myAnim;
  animation-duration: 1s;
}



@keyframes myAnim {
  0% {
    padding-left: 100%;
  }
  100% {
    padding-left: 0%;
  }
}

JavaScript (使用addEventListener) :

document.getElementById('1').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#1 catched animation end from #' + event.target.id + '! (Capturing)</br>');
}, true);

document.getElementById('2').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#2 catched animation end from #' + event.target.id + '! (Capturing) </br>');
}, true);

document.getElementById('3').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#3 catched animation end from #' + event.target.id + '! (Capturing) </br>');
}, true);

document.getElementById('1').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#1 catched animation end from #' + event.target.id + '! (Bubbling)</br>');
}, false);

document.getElementById('2').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#2 catched animation end from #' + event.target.id + '! (Bubbling) </br>');
}, false);

document.getElementById('3').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#3 catched animation end from #' + event.target.id + '! (Bubbling) </br>');
}, false);

其結果是:

2015年11月28日 星期六

Android - 使用MediaPlayer 播放音效

在Android中,MediaPlayer類別可以用來播放音效,只要給定音效源的Uri,不管是SD Card、手機中、還是網路上的聲音源,都可以利用其來播放,不過要注意的是,在Android 4.0以上規定了更嚴格的權限規範,所以如果聲音檔不是用正常管道放進SD Card的(例如:用IDE去強制放進去的),就不可以播放。

下面就來演示一個簡單的播放例子:

AndroidManifest.xml :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gjun.mediaplayerpractice" >

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>

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

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

</manifest>


MainActivity :
package com.example.gjun.mediaplayerpractice;

import android.media.MediaPlayer;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.SeekBar;
import android.widget.Toast;

import java.io.IOException;

public class MainActivity extends ActionBarActivity {


    private SeekBar control;
    private MediaPlayer mediaPlayer;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //取得元件
        control = (SeekBar) findViewById(R.id.control);
        // 註冊SeekBar元件進度改變事件
        control.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                // 一定要判斷是使用者的操作,因為播放過程也會更改進度
                if (fromUser) {
                    // 移動音樂到指定的進度
                    mediaPlayer.seekTo(progress);
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }

    public void clickPlay(View view) {
        // 開始播放
        if (mediaPlayer != null){
            mediaPlayer.start();
            // 執行顯示播放進度的AsyncTask物件
            new MyPlayTask().execute();
        }else if (!mediaPlayer.isPlaying()){
            new NetworkTask().execute("http://www.ntust20140311ai2.comuv.com/song01.mp3");
        }
    }

    public void clickPause(View view) {
        // 暫停播放
        if (mediaPlayer != null && mediaPlayer.isPlaying()){
            mediaPlayer.pause();
        }
    }

    public void clickStop(View view) {
        // 停止播放
        if (mediaPlayer != null){
            mediaPlayer.stop();
            // 回到開始的位置
            mediaPlayer.seekTo(0);
            control.setProgress(0);
        }
    }

    // 從指定的網路資源載入音樂並開始播放
    private class NetworkTask extends AsyncTask<String, Void, Void> {
        @Override
        protected Void doInBackground(String... networkPah) {
            // 建立網路資源音樂檔案Uri物件
            Uri uri = Uri.parse(networkPah[0]);
            mediaPlayer = MediaPlayer.create(MainActivity.this,uri);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);

            control.setMax(mediaPlayer.getDuration());
            // 註冊播放完畢監聽事件
            // 切換按鈕為可播放
            mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mediaPlayer) {
                    clickStop(null);
                    Toast.makeText(MainActivity.this, "Play End!",Toast.LENGTH_SHORT).show();
                }
            });
            // 開始播放
            mediaPlayer.start();
            Toast.makeText(MainActivity.this,"Play Start!",Toast.LENGTH_SHORT).show();
        }
    }

    // 在播放過程中顯示播放進度
    private class MyPlayTask extends AsyncTask<Void, Integer, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            while (mediaPlayer.isPlaying()){
                this.publishProgress(mediaPlayer.getCurrentPosition());
            }
            return null;
        }
        // 設定播放進度
        @Override
        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            control.setProgress(progress[0]);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}


activity_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="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/retangle_drawable"
        android:layout_margin="6sp"
        android:padding="6sp" >
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/play_icon"
            android:onClick="clickPlay" />
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/pause_icon"
            android:onClick="clickPause" />
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/stop_icon"
            android:onClick="clickStop" />
        <SeekBar
            android:id="@+id/control"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/retangle_drawable"
        android:layout_margin="6sp"
        android:padding="6sp" >

        <ImageButton
            android:id="@+id/record_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/record_dark_icon"
            android:onClick="clickRecord" />
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/play_icon"
            android:onClick="clickRecordPlay" />
        <ProgressBar
            android:id="@+id/record_volumn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="6dp"
            android:layout_marginRight="6dp"
            android:max="15"
            style="@android:style/Widget.ProgressBar.Horizontal" />
    </LinearLayout>

</LinearLayout>

源碼下載:
MediaPlayerPractice.7z

2015年11月21日 星期六

Android - 使用SoundPool播放短音效

有時候我們在寫Android程式時,只是想使用一些短小的音效,
然後利用其做變化來達到不同的效果,例如槍聲,我們只要移用一發的槍聲,
配上次數、間隔、持續時間、頻率(指聲波頻率)等,
就可以組成許多不同的槍聲,
這時我們可能就會選擇不用存一堆龐大的音效在手機中,而是存短小的一發槍聲音效就好,以節省記憶體空間。

Android 有一個類別就可以很好地提供這樣的需求,
SoundPool,
它可以存放多個音效,選擇要播放的音效,指定左、右聲道音量大小(給耳機用才聽得出來)、播放次數、持續時間等。

下面就來演示一個簡單的範例:

MainActivity.java :
package com.example.gjun.audiopractice;

import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.SeekBar;

public class MainActivity extends ActionBarActivity {
    //本例適用於智小音樂,可調節聲音大小及播放時間(只是拉長波型)

    // 控制左右聲道用的SeekBar元件
    private SeekBar volume_left, volume_right;
    // 左右聲道音量,0.0F ~ 1.0F
    private float leftVolume = 1.0F, rightVolume = 1.0F;
    //存放音樂的Pool
    private SoundPool soundPool;
    private int soundId01, soundId02;
    //現在或最近一次播放的soudPool音效ID
    private int playing;

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

        //獲取元件
        volume_left = (SeekBar) findViewById(R.id.volume_left);
        volume_right = (SeekBar) findViewById(R.id.volume_right);

        // 建立控制左右聲道的進度改變監聽物件
        SeekBar.OnSeekBarChangeListener listener =
                new SeekBar.OnSeekBarChangeListener() {
                    @Override
                    public void onProgressChanged(SeekBar seekBar,
                                                  int progress, boolean fromUser) {
                        switch (seekBar.getId()){
                            // 改變左聲道音量
                            case R.id.volume_left :
                                leftVolume = progress/10.0F;
                                break;
                            // 改變右聲道音量
                            case R.id.volume_right :
                                rightVolume = progress/10.0F;
                                break;
                        }
                        // 設定指定編號的左右聲道音量
                        soundPool.setVolume(playing,leftVolume,rightVolume);
                    }

                    @Override
                    public void onStartTrackingTouch(SeekBar seekBar) {}

                    @Override
                    public void onStopTrackingTouch(SeekBar seekBar) {}
                };
        //設置SeekBar的Listener
        volume_left.setOnSeekBarChangeListener(listener);
        volume_right.setOnSeekBarChangeListener(listener);

        // 建立SoundPool物件
        // 第一個參數設定音效最大數量
        // 第二個參數設定音效種類,通常指定為AudioManager.STREAM_MUSIC
        // 第三個參數設定播放音質,目前沒有作用
        // 載入指定資源編號的音樂並取得編號,第三個參數目前沒有作用
        soundPool = new SoundPool(3, AudioManager.STREAM_MUSIC,0);
        soundId01 = soundPool.load(this,R.raw.sound01,0);
        soundId02 = soundPool.load(this,R.raw.sound02,0);
    }

    //以下按下播放事件用activity_main.xml綁定在元件的click事件上
    public void clickPlay01(View view) {
        // 播放第一個音效
        soundPool.play(soundId01,leftVolume,rightVolume,0,0,1.0F);
        playing = soundId01;
    }

    public void clickPlay02(View view) {
        // 播放第二個音效
        soundPool.play(soundId02,leftVolume,rightVolume,0,0,1.0F);
        playing = soundId02;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}


activity_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="match_parent"
    android:layout_height="match_parent">

    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/retangle_drawable"
        android:layout_margin="12dp"
        android:padding="8dp"
        android:stretchColumns="1" >

        <TableRow
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp" >
            <TextView android:text="LEFT" />

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

        <TableRow
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp" >
            <TextView android:text="RIGHT" />

            <SeekBar
                android:id="@+id/volume_right"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:max="10"
                android:progress="10" />
        </TableRow>
    </TableLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/retangle_drawable"
        android:layout_margin="12dp"
        android:padding="6sp" >
        <ImageButton
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@drawable/play_icon"
            android:onClick="clickPlay01" />
        <ImageButton
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@drawable/play_icon"
            android:onClick="clickPlay02" />
    </LinearLayout>

</LinearLayout>

AndroidManifest.xml :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gjun.audiopractice" >

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

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

</manifest>

程式碼下載:
AudioPractice.7z

Android Google Map應用 - 使用Android Studio

這裡展示了在Android中使用Google Map的幾個簡單應用:

  1. 貼圖標 (自定內容不含外框、自定樣式含內容和圖標)
  2. 繪置線
  3. 繪置面(區域)
直接看程式碼:

MainActivity.java :

package com.example.gjun.googlemapmarkerpractice;

import android.graphics.Color;
import android.media.Image;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CircleOptions;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.Polygon;
import com.google.android.gms.maps.model.PolygonOptions;
import com.google.android.gms.maps.model.PolylineOptions;

public class MainActivity extends ActionBarActivity {
    private GoogleMap map;
    private LinearLayout info_panel;
    private TextView info;
    private Marker myMarker1;
    private Marker myMarker2;

    //設定地圖中心點
    private static final LatLng mapCenter = new LatLng(25.051234, 121.538315);

    // 繪製線條用的座標
    private static final LatLng station01 = new LatLng(25.04657, 121.51763);
    private static final LatLng station02 = new LatLng(25.044937, 121.523037);
    private static final LatLng station03 = new LatLng(25.042293, 121.532907);
    private static final LatLng station04 = new LatLng(25.051702, 121.532907);
    private static final LatLng station05 = new LatLng(25.05971, 121.533251);

    // 繪製區塊用的座標
    private static final LatLng station06 = new LatLng(25.062354, 121.541061);
    private static final LatLng station07 = new LatLng(25.041671, 121.5378);
    private static final LatLng station08 = new LatLng(25.04136, 121.557713);
    private static final LatLng station09 = new LatLng(25.063054, 121.552048);

    private Marker marker01, marker02, marker03, marker04, marker05;

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

        ///取得GoogleMap物件
        if (map == null) {
            map = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap();
        }
        //移動到地圖中心
        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(mapCenter, 13);
        map.moveCamera(cameraUpdate);

        //--------------------------繪制圖標--------------------------
        MarkerOptions markerOptions1 = new MarkerOptions();
        markerOptions1.position(station01)
                .icon(BitmapDescriptorFactory.fromResource(R.drawable.train));
        myMarker1 = map.addMarker(markerOptions1);

        MarkerOptions markerOptions2 = new MarkerOptions();
        markerOptions2.position(station05)
                .icon(BitmapDescriptorFactory.fromResource(R.drawable.taipei_101_marker));
        myMarker2 = map.addMarker(markerOptions2);
        myMarker2.setDraggable(true);
        //設置圖標的氣泡顯示訊息樣式
        map.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
            @Override
            //自訂樣式,連外框
            public View getInfoWindow(Marker marker) {
                if (marker.equals(myMarker2)) {
                    //從指定的畫面layout檔建立訊息視窗畫面物件
                    View view = getLayoutInflater().inflate(R.layout.info_window, null);
                    //說定圖示、標題和說明
                    ImageView badge = (ImageView) view.findViewById(R.id.badge);
                    TextView title = (TextView) view.findViewById(R.id.title);
                    TextView snippet = (TextView) view.findViewById(R.id.snippet);

                    badge.setImageResource(R.drawable.roc_flag);
                    title.setText("This is title!");
                    snippet.setText("This is snippet");

                    ///傳回自訂的訊息視窗
                    return view;
                }
                return null;
            }

            //自訂樣式,不連外框,只有內容
            @Override
            public View getInfoContents(Marker marker) {
                if (marker.equals(myMarker1)) {
                    //從指定的畫面layout檔建立訊息視窗畫面物件
                    View view = getLayoutInflater().inflate(R.layout.info_content, null);
                    //說定圖示、標題和說明
                    ImageView badge = (ImageView) view.findViewById(R.id.badge);
                    TextView title = (TextView) view.findViewById(R.id.title);

                    badge.setImageResource(R.drawable.train);
                    title.setText("This is title!");

                    ///傳回自訂的訊息內容
                    return view;
                }
                return null;
            }
        });
        //--------------------------------------------------------------------

        // --------------------------繪製點連成的線--------------------
        PolylineOptions polylineOptions = new PolylineOptions();
        polylineOptions.add(station01, station02, station03, station04, station05);
        polylineOptions.width(10);
        polylineOptions.color(Color.BLUE);
        map.addPolyline(polylineOptions);
        //--------------------------------------------------------------------

        //--------------------------繪製點連成的面--------------------
        PolygonOptions polygonOptions = new PolygonOptions();
        polygonOptions.add(station06, station07, station08, station09);
        Polygon polygon = map.addPolygon(polygonOptions);
        polygon.setStrokeWidth(5);
        polygon.setStrokeColor(Color.rgb(102, 153, 0));
        polygon.setFillColor(Color.argb(200, 255, 255, 0));
        polygon.setZIndex(1);
        //--------------------------------------------------------------------

        //--------------------------繪制圓形--------------------------
        CircleOptions circleOptions = new CircleOptions();
        circleOptions.center(mapCenter)
                .radius(2500)
                .zIndex(2)
                .strokeWidth(0)
                .strokeColor(Color.argb(200, 255, 0, 0))
                .fillColor(Color.argb(50, 255, 0, 0));
        map.addCircle(circleOptions);
        //--------------------------------------------------------------------
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

activity_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <fragment
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>


info_content.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <!-- 圖示 -->
    <ImageView
        android:id="@+id/badge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="5dp"
        android:adjustViewBounds="true"
        android:src="@drawable/train" >
    </ImageView>

    <!-- 標題 -->
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:ellipsize="end"
        android:singleLine="true"
        android:textColor="#ffff0000"
        android:textSize="14dp"
        android:textStyle="bold" />

</LinearLayout>

info_window.xml :
<?xml version="1.0" encoding="utf-8"?>
<!-- 設定背景為自己設計的外框圖形info_bubble -->
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/info_bubble"
    android:orientation="horizontal" >

    <!-- 圖示 -->
    <ImageView
        android:id="@+id/badge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="5dp"
        android:adjustViewBounds="true" />

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

        <!-- 標題 -->
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:ellipsize="end"
            android:singleLine="true"
            android:textColor="#ff000000"
            android:textSize="14dp"
            android:textStyle="bold" />

        <!-- 說明 -->
        <TextView
            android:id="@+id/snippet"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:singleLine="true"
            android:textColor="#ff7f7f7f"
            android:textSize="14dp" />
    </LinearLayout>

</LinearLayout>

build.gradle(Module.app):
apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.example.gjun.googlemapmarkerpractice"
        minSdkVersion 15
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'com.google.android.gms:play-services:+'
}

AndroidManifest.xml :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gjun.googlemapmarkerpractice" >

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >

        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="AIzaSyCBRZamFCDSFNgkk4CKbUBoePEgYvf87FE"/>

        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

    </application>

</manifest>

成品:


源碼下載:
GoogleMapMarkerPractice.7z

2015年11月7日 星期六

Google Play Service的Google Map - 使用Android Studio

這裡記錄在Android Studio中使用Google Map的方法:
  1. 設定Google Map的相依性(gradle dependency):

  2. 在module層級的build.gradle中,其dependencies中多加一行如下設定:

    compile 'com.google.android.gms:play-services:+'

     
  3. 設定權限及API Key:
    1. 在AndroidManifest.xml中設定權限,如下:
    2. 
      
      
      
      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
      
      
      
      
    3. <application>節點內設定API Key:
    4. 
      
      <meta-data    android:name="com.google.android.maps.v2.API_KEY"    android:value="Your_API_Key"/>
這樣就可以開始使用Google Map了,例如下面是一個簡單的顯示Google Map的例子,使用了SupportMapFragment (API level 12(Android 3.1) 以上可使用 MapFragment):


  1. 首先是Layout, activity_main.xml :
  2. <?xml version="1.0" encoding="utf-8"?>
     <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
       xmlns:tools="http://schemas.android.com/tools" 
       android:layout_width="match_parent"    
       android:layout_height="match_parent" 
       android:paddingLeft="@dimen/activity_horizontal_margin"    
       android:paddingRight="@dimen/activity_horizontal_margin"    
       android:paddingTop="@dimen/activity_vertical_margin"    
       android:paddingBottom="@dimen/activity_vertical_margin" 
       tools:context=".MainActivity">
          <fragment android:id="@+id/map" 
         android:name="com.google.android.gms.maps.SupportMapFragment" 
         android:layout_width="match_parent"       
         android:layout_height="match_parent" />    
    </RelativeLayout>
    
    
    
    
  3. 其實這樣就可以了,在不打程式碼的情況下,足以在畫面上顯示一個地圖版面。

2015年11月2日 星期一

Javascript的變量聲明提昇

最近學到了JavaScript的作用域與變量聲明提昇的觀念,
深感與C語言及Java有很大的不同,
如果不是一開始就學JavaScript的人非常可能會把以前的觀念代入而遭遇匪夷所思的錯誤,
所以特地在此做個紀錄。

在JavaScript中,有兩個跟C語言及Java非常不同的特性,

  1. 變量的作用域不為塊級作用域(block-level scope),而且支持函數作用域(function-level scope)。
  2. 在作用域裡會有變量聲明提昇的現象。
下面做個說明:

1.變量的作用域不為塊級作用域(block-level scope),而且支持函數作用域(function-level scope)

在C語言及Java中,只要在括弧( "()" 及 "{}") 中宣告的變數都屬於區域中的區域變數,例如以下程式碼:

int x = 1;

if (true) {

     int x = 2;

     println(x);  //2

}

println(x);       //1

很顯然地應該輸出為 2和1 ,因為大括弧{}中的int x=2;跟外面的int x=1 無關,兩次println(x)的x是不一樣的。
但是在JavaScript中,變數不是使用塊級作用域的,而是函數作用域的,例如以下的程式碼:

var x = 1;

if (true) {

    var x = 2;

    alert(x);    //2

}

alert(x);        //2

結果會是兩次的alert(x)都會顯示2,這是因為在if的大括弧{}裡的var x=2; 的x就是 外層的var x=1;,兩個x並沒有被當成是不同的x,所以內外層的alert(x)的x都是一樣的x。

因為JavaScript支持函數作用域,所以如果用函數來圍往同名變數的宣告的話,就可以區分內外的同名變數,如以下程式碼:

var x = 1;

function myFun(){

     var x = 2;

     alert(x);               //2

}

myFun();

alert(x);                   / /1


就會先顯示2再顯示1,因為此時myFun()裡的x與外層的x是不同的x變數。


2.在作用域裡會有變量聲明提昇的現象

另外一個JavaScript中獨特的特性是變量聲明提昇,以下兩種宣告情形會變提昇至作用域的頂端,即先被執行:
         
            一、以var宣告變數的動作。
            二、函數的無等號宣告 (如: function myFun(){} 這種,其實就等於 var myFun = function(){})

例如以下程式碼:

function sum() {   
    try{
       alert(sum.arguments.length);   //sum為undefined ,出錯
       var sum = 0;
       alert(sum);
    }catch(error){
       alert(error);
    }
}
sum(1,2); 

會補捉到TypeError: Cannot read property 'arguments' of undefined的錯誤,這是因為變量聲明提昇的作用,在提昇之後,其實真正的程式碼執行狀況長得就像下面這樣:

function sum() {   
    try{
       var sum;                                     //變數的宣告被提至作用域頂端
       alert(sum.arguments.length);     //sum為undefined ,出錯
       sum = 0;
       alert(sum);
    }catch(error){
       alert(error);
    }
}
sum(1,2); 

此時就很容易看出Error發生的原因,因為在sum()中,先宣告了一個sum屬性但未賦予其值,是undefined的,所以在alert(sum.arguments.length)中的sum是undefined的,當然就沒有arguments等屬性了。

再看一個例子,現在使用function的非等號式宣告:

var x = 1;
function myFun(){<
     x = 2;
     return;
     function x(){};
}
myFun();
alert(x);

答案是輸出1,因為被變數聲明提昇後的程式碼長得像這樣:

var x = 1;
function myFun(){
     var x;
     x = 2;
     return;
     x = function){};
}
myFun();
alert(x);

所以myFun()跟本沒有影響到外層的var x = 1的x而輸出原來的1了。

參考資料:

  1. JavaScript Scoping and Hoisting
  2. 翻譯 - JavaScript中的作用域與變量聲明提升

2015年10月24日 星期六

自製jQuery的PlugIn(twinkle演示範例)

JQuery提供外掛(Plug-In)能力,可以讓使用者設計自己所需的功能,跟自己做一個客製化類別(Class)不一樣,外掛就相當於是jQuery物件能夠使用的功能,能夠用selector的方式直接得到jQuery物件然然後使用,就像顯示物件是 $("selector").show();一樣,我們也可以自己寫一個外掛來用,例如$("selector").myPlugInFun();

以下為在寫JQuery的Plug-In時,常使用的其中一種寫法(只是舉例,寫法因人而異):

(function($){

    //這裡可設制function或變數

    $.fn.extend({

           //這裡可設制function或變數

           myPlugInFun1 : function(options){

                                         //這裡可設制function或變數,跟Java或C很不一樣的是,

                                         //JavaScript的function也算一種Object,所以可以在function裡設

                                         //function屬性

                                         var defaultOptions1 = {

                                               option1 : "option1" ,

                                               option2 : "option2"

                                         };

                                          return this.each(function(){

                                              $.extend(defaultOptions,options);

                                              //..........Do something

                                          });

                                      } ,

           myPlugInFun2 : function(options){

                                            ...以此類推

                                      }

    });

})(jQuery);


下面我們來寫一個簡單的jQuery Plug-In,叫做twinkle(),其作用是讓選取的jQuery物件可以依自訂(當然也可以預設)的次數及間隔時間閃爍,且也可以設定執行完後的callback function及擁有串接的特性,相關的說明註解都在程式碼中,直接看程式碼:

2015年10月13日 星期二

在xml中儲存tag類型符號,使用

在XML中,任何Tag型式的東西都會被當成正規的XML結構節點,例如

<message><someText>myText</someText></message>

但是如果我們想要儲存在XML中的內容有Tag型式的東西,但又不希望被XML當成節點呢?例如我們想把一個網頁的html儲起來,內容像這樣:

<html>

<head></head>

<body><p>someText</p></body>

</html>


如果把上述的html直接寫進XML中就會出問題,這時我們需要用<![CDATA[...]]>標籤來告訴XML其包住的內容不要用XML的方式解析,像如下這樣:

<message>

      <someHtml>

      <![CDATA[<html><head></head><body><p>someText</p></body></html>]]>

      </someHtml>

</message>

下面是一些注意事項:
CDATA 區段的內容必須在 XML 內容所允許的字元範圍內;不能用這種方式逸出控制字元和相容性字元。 此外,CDATA 區段內不能出現序列 ]]>,因為這個序列會發出區段結束訊號。 這意味 CDATA 區段不可以是巢狀。 這個序列也出現在部分指令碼中。 在指令碼內,通常可使用 ] ]> 替代 ]]>
             from CDATA 區段 [XML 標準] 

參考資料:

  1. CDATA 區段 [XML 標準] 
  2. Ian 懶惰蟲筆記: CDATA 區段 - XML 標準

2015年10月11日 星期日

CSS3的動畫

這裡紀錄一下利用CSS做簡單動畫的方法:
在CSS裡,可以利用animation-name屬性來指定動畫,並且利用@keyframes檢設定動畫

下面是一個簡單的例子,設定動畫為一秒,使用myAnim的自訂影格,影格內容分三個時間點(0%,50%和100%),動畫為左移動:
html:
<div class="animClass">我在移動</div>


css:
.animClass {

      animation-name: myAnim;   

      animation-duration: 1s;     

    

    }



@keyframes myAnim {

  0% {

      padding-left: 100%;

  }

  50% {

      padding-left: 0%;

  }

  100% {

      padding-left: 100%;

  }

}


線上實例演示:
http://jsfiddle.net/rd0zsy78/1/

CSS的動畫使用方法就跟平常使用CSS定義的一樣,用CSS選擇器來為選擇的元素定義動畫,包括了動畫名稱(animation-name)、持續時間(animation-duration)、重覆幾次(animation-iteration-count)等屬性可以設定。

接著就要自已定義對應影片名的關鍵影格,設定不同時間點(用百分比算)的CSS變化。

下面的參考資料就有詳細的各種參數資料可查詢。

參考資料

  1. CSS 動畫
  2. 使用 CSS Animation 製作網頁上的動畫(隻要 CSS3,不用 JavaScript!)
  3. CSS參考手冊

2015年10月2日 星期五

使用SvnAnt來在Ant中以程式化方式執行SVN行為

今天來紀錄一下如何在Ant中使用SvnAnt來執行SVN的行為動作。

首先,要先去下載SvnAnt,選擇合適的版本下載解壓縮後,把lib資料夾裡的jar檔全部丟掉Ant的lib中,把path還境變數設好,就可以開始撰寫操作SVN的Ant程式碼了。

以下就展示一個簡單的範例:
作用為向SVN export資料出來。

build.xml:
<?xml version="1.0" ?>
<project name="test" default="svnTestTask">
        <!-- 參數設置 -->
 <property name="dir" value="D:\testSvnDir" />
 <property name="svn_User" value="USER_NAME" />
 <property name="svn_Password" value="PASSWORD" />
        <property name="svn_url" value="SVN_URL" />

        <!-- 主要的Target -->
 <target name="svnTestTask">
  <delete dir="${dir}" />            
  
  <taskdef name="svn" classname="org.tigris.subversion.svnant.SvnTask" />
  <svn javahl="true" username="${svn_User}" password="${svn_Password}" >
   <export srcUrl="${svn_url}" destPath="${dir}" />
                </svn>
 </target>
</project>


 把build.xml放在要執行的資料夾,並且用DOS指令到資料夾中用ant指令即可執行,當然你也可以寫一個bat檔來執行:

test_svnant.bat:
cd d:
ant

在官網中,有蠻詳細的指令使用說明可以參考:
http://subclipse.tigris.org/svnant/svntask.html#update

參考資料:

  1. 【Tomcat】Ant與build.xml
  2. 用ant实现SVN代码更新,部署
  3. 官方說明
  4. ant 自定义 task

2015年9月24日 星期四

特定位置浮動Element效果(jQuery)

今天要介紹的使用jQuery來製作"特定位置浮動Element效果"。

首先先看一下影片演示:

其中的紅色框框,我們希望在卷軸往下拉、越過紅色框框時紅色框框能夠停留在上方,而卷軸往上拉回去時,紅色框框能夠回到原位。

這樣的效果常被用在網站的選單上面,讓使用者瀏覽網站時,能夠一直都看得到選單。

程式實現的邏輯是這樣的:
  1. 先取得紅色框框的位置。
  2. 當卷軸滑動時,計算卷軸是否滑超過等於卷軸的位置。
  3. 如果超過的話,固定紅色框框的位置,將其z-index提高,為了避免下面的元素補上來,先製作一個假的、但透明看不見的紅色框框再固定原紅色框框;否則,不固定紅色框框的位置。將假的紅色框框刪掉。

下面上程式碼:
1. html部份:
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
<div class="bar">
This is the div class, top_tab_bar_bg.
</div>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
Other Content. Other Content. Other Content. Other Content. Other Content.</br>
2. CSS部份:
body{
    height : 1000px;
}

.bar{
    width : 300px;
    border-style:solid;
    background-color: red;
}

3.javascript部份:
 //offset().top 是相對於文檔的偏移量,不會因為滾動卷軸而有變化。
    //CSS裡的top屬性在position為fixed時,是對應到相對於所處螢幕的偏移量
    //,會因滾動卷軸而有變化。
    var barOffsetTop = $(".bar:not(.tempBar)").offset().top;
 var barMarginTop = parseInt($(".bar:not(.tempBar)").css("margin-top"));
 
   $(window).scroll(function() {
     var scrollTopAmount= $(this).scrollTop();     
        //因為bar的position被設成fixed時會使下面的元素跑上來,所以做一個透明的tempBar來填充原來
        //bar的位置
     if (scrollTopAmount >= barOffsetTop){
            //如果卷軸拉超過bar的原始offset.top(),將bar固定住
            //如果沒有tempBar,做一個tempBar
      if ($(".tempBar").length == 0){
       $(".bar").clone().addClass('tempBar').insertAfter(".bar").css({"opacity":"0","z-index":"auto"});
      }      
      $(".bar:not(.tempBar)").css({ "position": "fixed", "top": "-"+barMarginTop+"px" , "z-index" : "999"});
     }else{
            //如果卷軸拉沒有超過bar的原始offset.top(),將bar的css還原,刪除tempBar
      $(".tempBar").remove();
      $(".bar:not(.tempBar)").css({ "position": "static" , "z-index" : "auto"});
     }
   });

線上看成品:
http://jsfiddle.net/a33e97cg/

2015年9月2日 星期三

好用 jQuery 外掛

 各種jQuery外掛庫的搜尋網站:

  1. jQuery plugins

覺得好用的jQuery外掛:

  1. jQuery custom content scroller :為區塊加上滑動的側邊卷軸。
  2. fullPage.js:為網站增加上下滑動(section)及同section左右滑動(slide)的效果。

2015年8月31日 星期一

好用的Google JavaScript 瘦身器

我們在做網站開發時,通常會用到很多JavaScript的檔案,而可能會佔上專案的不少空間,這時就會有把JavaScript裡的內容進行編碼瘦身的需求,經過編碼後,JavaScript裡的一些變量、函式名等之類的值都會度換成沒有意義的文字,並且縮排、換行會全部被取消,所以編碼後的檔案容量大小可以大幅度地被減少。

網路上有許多的JavaScript瘦身器,這裡介紹一個好用的Google JavaScript 瘦身器,可以幫助我們對JavaScript檔進行編碼。

Closure Compiler
它有提供多種編碼的方式,例如線上貼上代碼進行編碼,或是用它提供的API來以程式的方式進行編碼等,下圖為線上貼上代碼進行編碼的擷圖:

其他的JavaScript瘦身器資料:

  1. 5 Excellent JavaScript Minification Tools to Improve your Code's Performance
  2. JS & CSS Minifier (Jane Marie Shepard 網友推薦)

2015年8月27日 星期四

JavaScript的同名函式問題及輸入參數的陣列特性

最近在工作中發現了一個以前沒有注意到的問題,找到了解決方法及解釋,所以特在此做了一個紀錄。

在Java中,很多人都應該有同名函式,不同函式簽名的多載Overload概念,但是在JavaScript中,是不能把這樣的觀念一併套用的。

我們來看一下以下的程式碼:
<html>
<body>

<script type="text/javascript">
function foo(a) {
   alert('foo(a) is called!');
}
function foo(a,b,c) {
    alert('foo(a,b,c) is called!');
}
foo('a');
</script>

</body>
</html>

如果有寫過Java但不熟JavaScript的話,很容易就會想說跳出的視窗應該是 "foo(a) is called!" 的吧,不過事實上並不是這樣的。

事實上跳出的事窗會是 "foo(a,b,c) is called!",也就是說 foo(a) 這個function跟本就沒被呼叫到,很顯然的,JavaScript並不會因為不同的方法簽名呼叫就使用不同方法簽名的方法,也就是沒有Java常見的多載(overload)特性。

其實在JavaScript中,函式的名稱只是一個類似指標(C語言)或參考(Java的reference)的查西,在每一次指定function內容及名稱時,名稱指向的位置都會改變,上述的例子中,第一次foo指向一個function及記憶體位置,而第二次又指向了另外一個function內容及記憶體位置,所以foo(a)就沒用了,最後foo代表的就是foo(a,b,c)。

在JavaScript中,呼叫方法function)時,輸入的參數是以類似陣列的方式傳進方法中的,所以其實方法簽明中的輸入參數個數並不是很重要,甚至可以傳入參數給方法簽名沒有輸入參數的方法(當然在方法簽名上標名變數名稱有利於程式易讀性及使用)。

如以下的程式碼是完全可以成功且正確的執行的:
<html>
<body>

<script type="text/javascript">
function sum() {   
   var inputArguments = sum.arguments;
   var answer = 0;
   for (i=0 ; i < inputArguments.length ; i++){
      answer = answer + inputArguments[i];
   }
   alert('輸入了' + inputArguments.length + '個數字,' + '總合為:' + answer);
}
sum(1);
sum(1,2);
sum(1,2,3);
</script>

</body>
</html>

執行後將可以看到三個視窗跳出來,分別顯示如下文字:
輸入了1個數字,總合為:1
輸入了2個數字,總合為:3
輸入了3個數字,總合為:6

可以發現,在JavaScript中,方法本身其實就像是一個物件,例如在上例中 sum() 本身就像是一個物件,可以用sum.arguments來存取輸入的參數,而輸入的參數也是像一個物件,可以存取其length及各元素的值。

而也因為 sum 就代表名為 sum 的方法,所以如果我們用方法的名稱去宣告一個新變數是會發生問題的,例如像下面這個例子,就會發生 TypeError: Cannot read property 'arguments' of undefined 的錯誤 ,因為此時sum()代表呼叫sum函式,而使用了 var sum=0 時,sum.arguments.length的sum變成指向一個sum()中的sum屬性了,所以是還沒defined的(這時真正執行的順序是var sum; --> alert(sum.arguments.length); --> alert(sum); 請參考 "Javascript的變量聲明提昇"):

<html>
<body>

<script type="text/javascript">
function sum() {   
    try{
       alert(sum.arguments.length);  //這裡的sum是指sum()中的sum屬性,但因沒有定義而會出錯
       var sum = 0;
       alert(sum);
    }catch(error){
       alert(error);
    }
}
sum(1,2); //加括號時,sum指的是名為sum的function
</script>

</body>
</html>

那正確的寫法是什麼呢?事實上argument這個屬性是可以直接在function裡使用而不用在前面特別指定function名稱的,所以這樣寫就不會報錯了:
<html>
<body>

<script type="text/javascript">
function sum() {   
    try{
       alert(arguments.length);
       var sum = 0;
       alert(sum);
    }catch(error){
       alert(error);
    }
}
sum(1,2);
//輸出為:
//2
//0
</script>

</body>
</html>


如果這樣的話,也就是沒有下var sum=0,即沒有跟sum()的名稱衝突的話,也是可以的,此時sum就是function了
<html>
<body>

<script type="text/javascript">
function sum() {   
    try{
       alert(arguments.length);
       var sum2 = 0;
       alert(sum);
    }catch(error){
       alert(error);
    }
}
sum(1,2);
//輸出為:
//2
//function sum() {   
//    try{
//       alert(arguments.length);
//       //alert(typeof(sum));
//       var sum2 = 0;
//       alert(sum);
//    }catch(error){
//       alert(error);
//    }
//}
</script>

</body>
</html>

參考資料:
  1. Javascript同名函式
  2. The arguments array- the secret to robust functions

2015年8月23日 星期日

Google Analytics API的使用

Google Analytics 是 Google 的一個服務,可以幫助開發者來追蹤分析網站的流量、來源、訪問的瀏覽器等資料,Google提供了一個親切的可視化管理介面,並且可以在上面設置各種功能及觀看報表。

但是對於開發者來說,這樣的服務開不夠,開發者有可能會希望利用程式的方式進行Google Analytics 的追蹤查詢,所以Google又提供了Google Analytics API 的服務,讓開發者可以如使用Java等方式用程式來訪問Google Analytics。

最近在工作時,發現Google Analytics API 從2.0版升級到了3.0版本,所以很多以前用2.0版寫的程式都沒辨法跑了,必須要用到3.0版的方式才行。

在這裡我把如何申請Google Analytics及如何使用Google Analytics API做了一個紀錄,以供日後查讀:

一、申請及使用Google Analytics
首先你要先跟Google申請Google Analytics的使用,登入Google Analytics的首頁後,如果是第一次使用Google Analytics,要先設定想要追蹤分析的網頁網址。如果已經設定過了,就會進入報表頁面,如下所示:


按下"管理",可以看到如下畫面,分成"帳戶"、"資源"和"資料檢視"三個部份。

帳戶:為使用Google Analytics的管理者,可以設定不同的權限。

資源:為要追蹤分析的網站、行動應用程式或裝置 (如資訊站或銷售點裝置),在"資源"-->"追蹤資訊"-->"追錝程式碼裡"可以看到Google提供的JavaScript程式碼,需要放到想要追蹤分析的網頁程式碼中。

2015年8月17日 星期一

將Tomcat配置至Eclipse

這裡紀錄了在Eclipse中如何配置Tomcat的步驟:
  1. 配置Server
  2. 在Eclipse的Window-->Preferences-->Server-->Runtime Environments中,將Tomcat設到Server runtime environments中,


    選擇你要設定的Tomcat 版本,


    並選擇Tomcat的根目錄位址


  3. 引入Tomcat的Jar檔
  4. 對專案按右鍵,選擇Properties-->Java Build Path,到Libraries分頁裡面把Tomcat的Jar檔引入進來,如下所示,其中這裡用到了Variable的設置,所以下圖的"TOMCAT_HOME"事實上代表了Tomcat的根目錄位置。





    1. 在Eclipse做Java EE時,要記得把要用到的外步Jar設定到在布署時要放到的地方,如下圖,對專案按右鍵,選擇Deployment Assembly-->Add,再選擇"Java Build Path Entries",然後把引入的Jar選進來,例如下圖設為將外部Jar檔在伂署時放到WEB-INF/lib中,即Tomcat Web規則的位置。
    參考資料:

    1. Eclipse 的 Tomcat 環境設定
    2. JavaWeb:报错信息The superclass "javax.servlet.http.HttpServlet" was not found on the Java Build Path
    3. 新建java Web 出现的问题 球指点 急急!!!!

    解決在Eclipse無法正常顯示Unicode字元的問題(使用Properties Editor插件)

    在使用Eclipse IDE時,有時會碰到Unicode字元無法正常地顯示,例如:

    "即將推出"       變成      "\u5373\u5c07\u63a8\u51fa"

    這時只要使用Properties Editor這個插件(Plug-In)就可以很好地解決這個問題,下面就詳述解決的步驟:


    在Eclipse中,在上方選單中按下 Help-->Install New Software


    接著按下 "Add" 按鈕,在 "Add Repository"視窗中的 "Location" 打上 插件的位置 "http://propedit.sourceforge.jp/eclipse/updates/ (現在改成這個了: https://free.nchc.org.tw/osdn//storage/g/p/pr/propedit/eclipse/updates/site.xml)" , "Name" 可自取,最後按下 "OK"。




    此時會出現可供下載的Properties Editor選項,最下面的為最新版,其他的是之前的版本。



    按著按下 "Finish" 後將其按裝完成。

    此時會需重新啟動Eclipse,之後就會發現所有有Unicode內容的Property檔案左邊出現了 綠色的"P"圖案,如下所示:
    這樣之後打開檔案就可以正常顯示Unicode字元了,當然你也可以對檔案按右鍵用"Open With"選傳統的Text Editor來看原始的字元編碼內容。


    2015年8月2日 星期日

    Spring的注解(Annotation)事務(Transaction)範例與JUnit測試(Spring+Hibernate+JUnit)

    此篇文以一個簡單完整的範例,來說明在Spring中如何使用注解(Annotation)事務(Transaction)與其要注意的事項,並且以JUnit來測試

    首先我們要下載 aopalliance.jar ,不然在執行事務時可能會發生ClassNotFoundException。可以到The Central Repository中搜尋最新版的jar,這是因為aopalliance.jar已經從NetBeans內建的Spring中分離了出來,要自已手動下載並引入至Library。如下圖:

    在這裡,我們想要設計的構想其專案配置如下:
    詳述如下:

    1. DaoClass.java:操控資料庫的CRUD(新增、查詢、修改、刪除)之Class。
    2. Guest.java:代表資料庫中的資料表之ORM(Object-Relational Mapping) 物件。
    3. ServiceClass.java:使用DaoClass進行事務(Transaction)處理的Class。
    4. DaoTest.java:用來測試DaoClass的測試JUnit。
    5. ServiceTest:用來測試ServiceClass的測試JUnit。
    在這裡,Guest代表的Table設計如下圖所示(在此例中Table中不用儲任何紀錄,這裡只是做個示範):


    首先是在applicationContext.xml中配置要給Spring托管的內容,如連線池、SessionFactory、Annotation的支持設定、自動裝配Bean等等:

    2015年7月16日 星期四

    Spring MVC中Controller與View的傳值

    Spring MVC中很有趣的地方就是Controller可以為一個POJO或是一個普通的Java Bean,不用繼承如HttpServlet等類別,而要將請求導向View(例如JSP)時,也只要回傳String或ModeAndView等,不用使用像response.sendRedirect()或request.getRequestDispatcher("XXX").forward(request, response)等語法,使得單元測試變得更為簡單與容易。

    那麼View和Controller之間要怎麼溝通呢?在這裡以一個簡單的例子實作來展示一些在Spring中View和Controller之間溝通的方式。

    例子要實做的內容為,使用者一開始會進入一個表單輸入頁面,可以輸入名字及選擇稱謂(先生或小姐),按下送出按鈕後,會引導到招呼畫面,其中招呼內容包含輸入的名字及稱謂,而如果名字為雨果及稱謂為先生的話,招呼畫面的內容會稍有不同。

    首先先來看專案的配置:



    其中包括:

    1. dispatcher-servlet.xml:Spring的dispatcherServlet設定檔。
    2. FormView.html:一開始的表單輸入頁面。
    3. FormCheckResultView.jsp:招呼畫面。
    4. FormController.java:接收及轉送參數的Controller。
    在web.xml中,我們讓所有的url請求都先讓Spring處理:

    2015年7月15日 星期三

    Java Swing的DnD (Drag and Drop)

    Drag and Drop (DnD) 就是中文的拖放,例如我們平常在Windows下常常會用滑鼠直接將檔案從一個資料夾移到另一個資料夾、或是將影片檔直接移到播放器的介面上以播放影片,這些動作就是所謂的DnD,而在Java Swing裡也提供了DnD的實作功能。

    在這篇文章中,我們要演示一個簡單的DnD實作,在主要Swing畫面上放置一個JTextArea,當我們在Windows下把一個或多個檔案用滑鼠圈選移到JTextArea後,JTextArea裡會顯示所移上去的檔案路徑列表。

    程式非常簡單,首先我們可以建立一個Java Swing的專案(例如使用NetBeans),命名為 "DragAndDropTest"。並在上面放一個JTextArea,我們把它命名為 "dragAndDropJTextArea"。
    接著在DragAndDropTest的建構子中加上如下程式碼即可:

    public DragAndDropTest() {
            //產生各元件
            initComponents();
            //為dragAndDropJTextArea設置DropTarget,其用來接收DropTargetDropEvent,
            //可從DropTargetDropEvent取得Transferable物件,即被包裝的拖曳物件。
            dragAndDropJTextArea.setDropTarget(new DropTarget() {
                public synchronized void drop(DropTargetDropEvent evt) {
                    try {
                        //設置可接受的拖曳類型
                        //如果拖曳物件可支援檔案列表格式(javaFileListFlavor)
                        if (evt.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                            //接受拖曳
                            evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
                            //得到Transferable物件,並將其轉成檔案列表格式(javaFileListFlavor),
                            //轉型成List<File>
                            List<File> droppedFiles = (List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
                            //清空dragAndDropJTestArea的內容
                            dragAndDropJTextArea.setText("");
                            //將拖曳的檔案以檔案列表的方式顯示在dragAndDropJTestArea中 
                            //字顏色為黑色
                            dragAndDropJTextArea.setForeground(Color.BLACK);
                            for (File file : droppedFiles) {
                                //顯示檔案列表
                                dragAndDropJTextArea.append(file.getPath() + "\n");
                            }
                        } else {
                            //若拖曳的物件不能支持檔案列表格式,
                            //拒絕Drop
                            evt.rejectDrop();
                            //顯示錯誤訊息,字顏色為紅色
                            dragAndDropJTextArea.setText("錯誤:拖曳類型不支援檔案列表格式!");
                            dragAndDropJTextArea.setForeground(Color.red);
                        }
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            });
        }
    

    最後試試看結果:

    1. 如果拖曳的是一般檔案,即可在JTextArea中顯示檔案列表,如下所示:


    2.  如果拖曳的是不可轉成檔案列表格式的物件,則在JTextArea中會顯示錯誤訊息:


    最後附上源始碼下載:
    DnDTest.7z

    參考資料:

    1. How to drag and drop with Java 2
    2. Java中的Drag and Drop詳解與代碼示例
    3. java swing中實現拖拽功能示例_java

    2015年7月12日 星期日

    JSP/Servlet中web.xml裡的<welcome-file>與Spring MVC的<mvc:resources>

    在使用Spring框架時,如果在web.xml裡將所有的請求都交給Spring來處理的話,例如下面這樣:
    <servlet>
            <servlet-name>dispatcher</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <load-on-startup>2</load-on-startup>
        </servlet>
    <servlet-mapping>
            <servlet-name>dispatcher</servlet-name>
            <url-pattern>/</url-pattern>
    </servlet-mapping>
    

    此時一些靜態資源可能就會因為沒有相應的Url Mapping或Controller來處理而無法回應靜態資源(如html, jpg, css, js等)的請求,這時我們可以有兩種方法來解決:
    1. 在Spring設定檔中,例如dispatcher-servlet.xml,加入如下內容,作用為再交由Spring的url mapping等處理之前,如果是靜態資源就交還給Web應用服務器管理:

    2. <mvc:default-servlet-handler />
      

    3.  在Spring設定檔中,加入以下內容,作用為對於符合"mapping"的靜態資源邏輯路徑(**表示任意子路徑),告知Web應用服務器去"location"的地方查找。例如對於以下設定,邏輯路徑為/resource/img/picture.jpg就會去找/img/picture.jpg:

    4. <mvc:resources location="/" mapping="/resource/**"/>
      
    這裡要注意的地方是,如果我們有在web.xml裡設定歡迎頁面,例如像下面這樣:

    <welcome-file-list>
            <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    

    然後請求Web應用程式的根目錄時,例如 http://XXX.XXX.XXX.XXX/projectName ,根目錄的url請求,即 "/" ,會被Spring所接收處理,假設Spring設定檔中設定如下:

    <mvc:resources location="/" mapping="/**"/>
    

    根目錄 "/" 將會與mapping內容符合,並以location的值 "/" 返回給Web應用服務器,Web應用服務器發現為根目錄("/")的請求時,就會根據web.xml裡的歡迎頁面設定查找index.html。

    也就是說,如果我們現在把index.html放在 "/html/index.html" 的路徑中,並且想要以http://XXX.XXX.XXX.XXX/projectName 的請求找到index.html的話,以下的設定方法是錯的

    2015年7月9日 星期四

    整合Spring、Hibernate和C3P0的配置

    當我們只有使用Hibernate的時候,其中要獲取SessionFactory的話必須要使用Hibernate的API手動獲取,例如像這樣:

    SessionFactory sessionFactory = AnnotationConfiguration().configure().buildSessionFactory();
    

    如果配合Spring的話,就可以使用注入的概念將SessionFactory注入到要使用SessionFactory的類別中。

    整合Spring與Hibernate有兩種方法:

    1. 在Spring配置中直接將原Hibernate的配置檔抓進來。
    2. 在Spring自己配置Hibernate的配置、連接池(例如C3P0)等,可以捨棄原來的Hibernate配置檔,也可把它抓進來,然後覆蓋部份配置。

    讓我們再一次看一次原來的Hibernate配置檔,hibernate.cfg.xml:

    2015年7月8日 星期三

    Hibernate中C3P0連接池的設置

    在一開始使用Hibernate做ORM(Object/Relational Mapping)時,如果我們沒有配置連接池(Connection Pool),可能會發生一些底層連接Database會碰到的問題,例如sessionFactory所產生的連接太久沒有動作而被Database端以Timeout為由強行關閉,而之後在使用連接時就會出錯(在Hibernate端似乎無法用SessionFactory的isClosed()來判斷),此時就可能要自己在做CRUD(Create,Read,Update,Delete)時手動測試連接是否還在等等。

    如果配置了連接池,就可以簡單地受益於連接池帶來的許多好處,例如幫我們管理連線,最小連接數量、最大連接數量、定時檢測連線等等。

    在Hibernate裡已經包含了一個很好用且有名的開源數據庫連接池,C3P0,如果你在NetBeans裡使用了Hibernate框架,那麼你就可以在Library中看到C3P0的jar,如下圖紅框內所示:

    對於C3P0,以下的連結提供了不錯的介紹、範例與相闗資料:

    1. C3P0百度百科
    2. How to configure the C3P0 connection pool in Hibernate
    3. 關於MySQL的wait_timeout連接超時問題報錯解決方案
    4. c3p0 - JDBC3 Connection and Statement Pooling(官方網站)
    在這邊,我把我使用的例子在這邊做了一個紀錄,在Hibernate的配置文件hibernate.cfg.xml中,加入如以下配置,這樣Hibernate就會自動偵測並使用C3P0了:
            <!--==============C3P0的配置=============-->
            <!-- 最小連接數 -->
            <property name="hibernate.c3p0.min_size">5</property>
            <!-- 最大連接數 -->
            <property name="hibernate.c3p0.max_size">20</property>
            <!-- 多久會把無用的連接視為timeout並移除到min_size的連接數量,單位毫秒 -->
            <property name="hibernate.c3p0.timeout">300</property>
            <!-- 最大的PreparedStatement的數量 -->
            <property name="hibernate.c3p0.max_statements">50</property>
            <!-- 多久進行空閒連接的檢查,確定連接還存在,例如有無被Database端關掉,單位是秒-->
            <property name="hibernate.c3p0.idle_test_period">3000</property>
            <!--======================================-->
    

    注意:此處例出來的配置只是C3P0配置的一部份,其功能不只這些,其他的配置可以在上述的資料連接中查到。

    2015年7月7日 星期二

    Java - 在HttpGet及HttpPost中塞入資料參數的方法

    當我們想用HttpClient的方式進行HttpGet及HttpPost連線時,常會需要在其中放入一些資料參數,在這邊紀錄整理了一些個人覺得不錯的、如何向連線放入參數的方法:

    Note:
    這裡使用了以下 Maven dependency,其中官方說明
    commons-httpclient 官方已停止更新,
    改由 org.apache.httpcomponents 繼續提供更新
    (官方原文 :
    The Commons HttpClient project is now end of life, and is no longer being developed. It has been replaced by the Apache HttpComponents project in its HttpClient and HttpCore modules, which offer better performance and more flexibility.
    )

            <!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
    <dependency>
        <groupId>commons-httpclient</groupId>
        <artifactId>commons-httpclient</artifactId>
        <version>3.0.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.13</version>
    </dependency>

    HttpGet:
    HttpGet的參數都是放在Url後面的,例如"http://XXX.XXX.XXX.XXX?param1=1&param2=2",所以我們的目標就是創造出帶有參數的Url即可。
    參考資料:How to add parameters to a HTTP GET request in Android?

    1. 最簡單的方式就是手動在HttpGet的Url後面加上要的參數資料(限文字),例如直接自行組合字串 (param 的值可能需要做 url encode):
    2. String url = "http://XXX.XXX.XXX.XXX?" + "param1=" + param1 + "&param2=" + param2;
      
      
    3. 使用URIBuilder來幫助我們產生Uri:
    4. URI uri = new URIBuilder("http://XXX.XXX.XXX.XXX:8080/path1/path2/xxx.jsp")
          .addParameter("param1", param1)
          .addParameter("param2", param2)
          .addParameters(listOfParameters)  //如果參數以NamerValuePair放在
          //List<NameValuePair>中的話可以
          //使用addParameters()
          .build();
      
      以上也可也拆成多個方法變成如下的樣子:
      URI uri = new URIBuilder()
          .setScheme("http")
          .setHost("XXX.XXX.XXX.XXX")
          .setPort(8080)
          .setPath("/path1/path2/xxx.jsp")
          .addParameter("param1", param1)
          .addParameter("param2", param2)
          .addParameters(listOfParameters)
          .build();
      
      因為 addParameter() 已經會對輸入值做 url encode ,所以不用再對資料做一次 url encode
    HttpPost:
    HttpPost的參數都是放在Entity中的,所以我們只要用List<NameValuePair>的型式把資料放到HttpPost的Entity中就行了。
    參考資料:How to add parameters to a HTTP GET request in Android?

    String url = "http://XXX.XXX.XXX.XXX:8080/path1/path2/xxx.jsp";
    //建立HttpClient
    //DefaultHttpClient httpClient = new DefaultHttpClient(); //舊方法,現已 Deprecated
    CloseableHttpClient httpClient = HttpClients.createDefault();
    //建立HttpPost
    HttpPost httpPost = new HttpPost( url ) ;
    //建立要送的資料參數
    ArrayList<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
    params.add(new BasicNameValuePair("param1" , param1)) ;
    params.add(new BasicNameValuePair("param2" , param2)) ;
    //將資料參數放到HttpPost的Entity裡面,並指定編碼
    httpPost.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
    //Execute連線發送訊息並取得回應
    HttpResponse response = httpClient.execute(httpPost);
    System.out.println(EntityUtils.toString(response.getEntity(), "UTF-8"));
    
    或是使用PostMethod
    final HttpClient client = new HttpClient();
    final PostMethod postMethod = new PostMethod("http://XXX.XXX.XXX.XXX:8080/path1/path2/xxx.jsp");
    postMethod.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET,"utf-8");  
    postMethod.addParameter("param1", param1);
    postMethod.addParameter("param2", param2);
    //如果想將文字資料直接放到HttpPost的Entity裡面,可以這樣
    //postMethod.setRequestEntity(new StringRequestEntity("", ContentType.APPLICATION_JSON.toString(), StandardCharsets.UTF_8.name()));
    
    int statusCode = client.executeMethod(postMethod);
    System.out.println(postMethod.getStatusLine());
    System.out.println(postMethod.getResponseBodyAsString());