Search

Swift - Builder Pattern

2017-01-15 2:18 PM

Builder Pattern

在建立物件時的一種方法

比起一般一連串的 set...set...set

Builder Pattern 可以更優雅的創建具複雜屬性的物件

程式碼可讀性也會提高許多

程式碼範例
// 宣告
class DataBean {

    var id:int;
    var title:String;
    var desc:String;

    // 設定別名 可避免混淆物件來源
    typealias builderClosure = (DataBean) -> Void

    // 建構子
    init(build:builderClosure) {
        build(self);
    }
}

// 使用
var data = DataBean(build: {
                $0.id = 0;
                $0.title = "標題";
                $0.desc = "這是一個教學範例唷~";
           });
各項資料連結
Swift Tour

Swift - CocoaPods 安裝及使用方式教學

2017-01-06 6:15 PM

CocoaPods 可以看作是 Gradle 或 Maven 的 Swift 版本

能夠協助管理引用的 libraries

雖然沒有與 XCode 做介面上的整合

但使用方式與 Gradle 相當類似 因此很容易就可以上手

首先開啟終端機後輸入以下命令就會開始安裝

sudo gem install cocoapods

安裝完成後將目前的工作目錄切換到專案的根目錄底下

cd ~/AppCodeProject/MyProject

再輸入以下命令即可初始化專案

pod init

初始化完成後可以看到專案根目錄底下新增了一個 Podfile 檔案

這個檔案與 gradle.build 檔案是相當類似的一個存在

主要用來管理目前專案中有用到哪些 libraries

以下為加入了 Firebase libraries 的檔案範例

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'MyProject' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for MyProject
  pod 'Firebase/Core'
  pod 'Firebase/AdMob'
  pod 'Firebase/Messaging'
  pod 'Firebase/Crash'

  target 'MyProjectTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'MyProjectUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

其中的 # Pods for MyProject 就是可以加入 libraries 的地方

只要使用 pod 'LIBRARY_NAME' 就完成了加入的動作

之後再到終端機介面使用 pod install 即可開始下載需要的檔案

不過這邊要注意一點

如果是第一次使用 CocoaPods 建議使用以下步驟來做第一次的初始化動作

pod setup --verbose
pod install --verbose

setup 動作是初始化 master repo(--verbose 表示要輸出 log 可以明白目前的處理進度)

這個動作會將 git 上的所有檔案下載下來 需時非常久

若沒有經過這個動作而直接使用 pod install 可能會失敗或是卡住

如果說你已經卡在 pod install 的動作的話

可以先使用以下命令將原本的 master repo 移除 重新初始化

pod repo remove master

setup 動作完成後再使用 install 將 libraries 下載回來

以上動作完成之後會在專案根目錄內新增一個 MyProject.xcworkspace 的檔案

之後請使用這個檔案開啟專案(原本應該是使用 MyProject.xcodeproj)

開啟後可以在 XCode 的介面發現多了一個新的 Pods 專案

裡面包含著我們引入的所有 libraries

可以試著開啟一個 swift 檔案使用 import 看看是否可以成功

若在日後需要新增 library 只要再次編輯 Pod 檔案即可

編輯後使用 pod install 會下載新增的項目

而原本已存在的 libraries 則不會進人任何動作,所以不必擔心每次更新一大堆還要檢查相容信

但若是使用 pod update 則除了下載新增項目之外,還會額外檢查已存在的 libraries 是否有新的版本

這是 install 與 update 的最主要差異

請各位視情況選擇使用的命令,避免更新到尚未支援的版本造成困擾喔!

各項資料連結
CocoaPods

iOS - Swift 3 基礎語法介紹

2016-12-29 12:18 PM

此篇文章將介紹在 Apple iOS 平台開發使用的 Swift 3 基礎語法

在某些地方會使用 Java 的類似方法做比對

如此一來便能夠在某些程度上加速對 Apple iOS Swift 3 基礎語法上的了解(當然前提是你懂 Java 囉~)

// 若不使用 ; 結尾也沒問題 swift 使用斷行符號結尾

// 特性 1
// Swift 內的所有物件 除了 class 類別物件以外
// 其他物件全部都是 pass by value (struct, enum)
// 也就是說 在作為參數傳遞、指定值的時候 除了 class 類別物件以外
// 其他物件都會複製一份自己 此時若更改複製後的屬性值 將不會變更原本物件的屬性值
// array, dictionary, String, Int, Double 等等都是屬於 struct 的類別

// 特性 2
// struct/enum 若被指定給 constant 變數(let a = someStruct)
// 則此 struct/enum 將會變為 immutable 屬性值皆不可異動
// 而在作為參數傳遞時的所有變數都將會是常數( func perform( name:String, value:Int) name 及 value 都是常數 不可異動 )
// 此時若有可能會在方法內修改 struct/enum 的屬性值時 需要在 func 前方加入 mutating 保留字
// 即可異動 struct/enum 的屬性值(詳見 protocol)

// 特性 3
// Swift 會猜測變數型態 因此若無特殊需求 不需要在宣告變數時加入型態

// 印出字串

print("Hello, world!");

--

// 宣告變數

var name:Type = value;
var name1, name2, name3: Type

--

// 宣告常數

let name:Type = value;

--

// 宣告 optional

var name:Type? = nil;

// 宣告為 optional 可以將變數值設定為 nil
// 並可使用於 if 條件 判定是否為 nil 值(詳見 if 邏輯判斷式)
// 只有 optional 變數可以使用是否為 nil 的判斷

--

// 類型轉換 

var floatNum:Float = 2.353;
var intNum:Int = Int(floatNum);

// print(intNum); -> 2

var string:String = "There are \(intNum) pens.";

// print(string); -> There are 2 pens.

--

// 宣告陣列

var shoppingList = ["catfish", "water", "tulips", "blue paint"];

// 指定陣列值(若超過目前陣列長度將出現 indexOutOfBound 錯誤)

shoppingList[0] = "QQ";

// 新增陣列值

shoppingList.append("TT");
shoppingList += ["AA", "BB"];

// 插入陣列值

shoppingList.insert("CC", at: 2);

// 建立空陣列

var emptyArray:[String] = [String]();
var emptyArray:[String] = [];

--

// 宣告 dictionary

var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]

// 指定 dictionary 值(若不存在則新增至 dictionary)

occupations["QQ"] = "QQ";

// 建立空 dictionary

var emptyDictionary:[String: Float] = [String: Float]();
var emptyDictionary:[String: Float] = [:];

--

// Tuple
// 新型態與語法 可快速建立可儲存多個值的多個屬性
// 在 func 語法內有進階應用方式(return 值)

let x: (String, Int, Double) = ("Hello", 2, 0.85)
// set tuple name
let (word, number, value) = x

print(word)     // Hello
print(number)   // 2
print(value)    // 0.85

// with default name
let x:( word: String, number: Int, value: Double) = ("Hello", 2, 0.85)

print(x.word)     // Hello
print(x.number)   // 2
print(x.value)    // 0.85

// rename tuple
let (w, n, v) = x

print(w)   // Hello
print(n)   // 2
print(v)   // 0.85

--

// Range
// 儲存開始與結束 Index 的泛型物件

struct Range<T>{
    var startIndex: T
    var endIndex: T
}

// 且具簡易表示語法 ... 以及 ..<

// 範例: 取得陣列中的 subArray
let array = ["a", "b", "c", "d"]
var subArray1 = array[2...3] // c, d
var subArray2 = array[2..<3] // c

for i in 27...104 {} // Range 可以列舉(詳見 for 迴圈)

--

// 邏輯判斷式

var big:Int = 3;
var small:Int = 1;

if( big > small ){
    print("Yes");
}
else{
    print("No")
}

// print -> Yes

// optional 應用

var optionalString: String? = "Hello"
print(optionalString == nil)

var optionalName: String? = "John Appleseed"
var greeting = "Hello!"

// 若 optionalName 是 nil 則不會進入
if let name = optionalName {
    greeting = "Hello, \(name)"
}
else{
    greeting = "Hello, nobody"
}

// switch
// 與 java 不同的是 不需使用 break; 中斷 case 區塊
// 因為 swift 進入 case 後就不會再繼續往其他 case 執行了

let vegetable = "red pepper"
switch vegetable {
case "celery":
    print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
    print("That would make a good tea sandwich.")
// 將 vegetable 值指定給 x 並判斷 x 是否以 "pepper" 結尾
// 若此處直接使用 vegetable.hasSuffix 會出錯
// 用 let 將 vegetable 值指定給 x 常數後即可使用 switch 判斷
case let x where x.hasSuffix("pepper"):
    print("Is it a spicy \(x)?")
default:
    print("Everything tastes good in soup.")
}

// print -> Is it a spicy red pepper?

--

// 若值不存在 則給予初始值的方法

let nickName: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fullName)"

// print(informalGreeting); -> Hi John Appleseed

--

// 基礎 for 迴圈

var total = 0

// 等同於 for( int i = 0; i < 4; i++ )
for i in 0..<4 {
    total += i
}

print(total);

// print -> 6

// 列出成員 for 迴圈

let interestingNumbers = [
    "Prime": [2, 3, 5, 7, 11, 13],
    "Fibonacci": [1, 1, 2, 3, 5, 8],
    "Square": [1, 4, 9, 16, 25],
]
var largest = 0

// kind 為 key 值, numbers 則是 value 值
// 變數名稱自訂 dictionary 本身無排序
for (kind, numbers) in interestingNumbers {
    // 使用 in 依序列出陣列成魚
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print(largest)

// print -> 25

// while 迴圈
// 條件成立才會進入區塊

var n = 2
while n < 100 {
    n = n * 2
}
print(n)

// print -> 128

// repeat while 迴圈
// 至少執行一次 條件成立才會執行下一次

var m = 2
repeat {
    m = m * 2
} while m < 100

print(m)

// print -> 128

--

// 宣告方法

// func 保留字 
// greet 自訂方法名稱
// person: String 變數名稱及類型
// -> String 方法回傳值類型 若無回傳值則不需加入
func greet(person: String, day: String) -> String {
    return "Hello \(person), today is \(day)."
}
greet(person: "Bob", day: "Tuesday")

// 不需回傳值的方法
func greet() {
    print("Testing");
}
greet();

// 使用自訂變數標籤(Label)
// 預設將會使用變數名稱作為標籤
// 若要自訂可在參數名稱前空一格後加入 或是使用 _ 表示不需使用標籤

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
greet("John", on: "Wednesday")

// 回傳 不定數量的多個數值(tuple)

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
    var min = scores[0]
    var max = scores[0]
    var sum = 0
    
    for score in scores {
        if score > max {
            max = score
        } else if score < min {
            min = score
        }
        sum += score
    }
    
    return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
// 可使用名稱取值
print(statistics.sum) // 120
// 或是使用索引值取值
print(statistics.2) // 120


// 傳入不定數量的多個數值

func sumOf(numbers: Int...) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}
sumOf() // 0 
sumOf(numbers: 42, 597, 12) // 651

// 巢狀方法
// 可以在方法內再宣告一個方法

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
returnFifteen() // 15

// 使用其他方法作為方法的參數值

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [20, 19, 7, 12]
hasAnyMatches(list: numbers, condition: lessThanTen)

// 傳入使用匿名方法參數
// 匿名方法不需宣告名稱及加入 func 保留字
// 僅需定義參數及回傳值即可 但後方需使用 in 保留字作為區塊宣告(相對於一般方法使用的 {})

// map 為傳入方法參數 自訂轉換功能(例如轉換大小寫、更改數值) 回傳轉換後的陣列(原始陣列值不變)

// 所有數值 * 3
var numbers = [20, 19, 7, 12]

numbers.map({
    (number: Int) -> Int in
    let result = 3 * number
    return result
})

// 更簡易的寫法(由於參數類型已知 回傳類型亦同 因此一併忽略)
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)

// 所有字串改為大寫
var strings = ["a", "b", "c"];

strings.map({
    (string:String) -> String in
    return s.uppercased()
})

// 更簡易的寫法(由於參數類型已知 回傳類型亦同 因此一併忽略)
strings.map({ $0.uppercased(); })

// 亦可用於排序方面
let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)

// 或是篩選陣列中的值 並將所有回傳 true 的值重新組成一個新的陣列
let array = [2, 7, 15, 36, 87, 104]
print( array.filter({ $0 > 20 }) ) // [36, 87, 104]

// 也可以將 array 內的值做一個運算
let array = [1,2,3,4,5]
print( array.reduce(0){ $0 + $1} ) // 15

--

// 宣告 class

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

// 具有建構子的 class
// 建構子為不需加入 func 保留字也不需宣告回傳值類型的方法 名稱必須為 init
// 建立物件時必須傳入參數 var ns = NamedShape(name: "PiTT");
// 若加入 deinit 則是當物件要從記憶體內移除時自動被呼叫的方法
// 可以在此處 release 某些物件

class NamedShape {
    var numberOfSides: Int = 0
    var name: String
    
    // 建構子
    init(name: String) {
        self.name = name
    }
    
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

// 若使用 init? 則表示初始化過程可能會回傳 nil
// 也就是回傳一個 Optional 物件

class NamedShape{
    var numberOfSides: Int = 0
    var name: String
    
    // 建構子
    init?(name: String) {
        if( name.isEmpty ) return nil
        self.name = name
    }
    
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

// 繼承 class
// 在後方加入父類別 class

class Square: NamedShape {
    // 使用 override 保留字表示此為覆寫父類別方法的方法
    // 若未加入 override 保留字 同時方法名稱、傳入參數及回傳直接相同的話
    // IDE 會直接報錯
    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}

// 使用欄位的 getter/setter

class Square: NamedShape {
    var sideLength: Double = 3.0;

    var perimeter: Double {
        get {
            return 3.0 * sideLength
        }
        set {
            // newValue 表示傳入的參數(等號後方的值)
            // set 本身已隱含此預設名稱 set(newValue)
            // 亦可自定義名稱 使用 set(value)
            sideLength = newValue / 3.0
        }
    }
}

var s: Square = Square();

// print(s.perimeter) -> 9.0

// 使用 willSet
// willSet 表示當欄位值在 init 之外改變值的時候都會呼叫的方法
// 例如以下範例為保證兩個欄位值的邊長皆相同的方法

class TriangleAndSquare {
    var triangle: EquilateralTriangle {
        willSet {
            square.sideLength = newValue.sideLength
        }
    }
    var square: Square {
        willSet {
            triangle.sideLength = newValue.sideLength
        }
    }
    init(size: Double, name: String) {
        // 此處設定欄位值並不會觸發 willSet
        // 所以若修改為 square = Square(sideLength: size * 2, name: name)
        // 則兩個欄位的邊長將會不相等
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size, name: name)
    }
}

var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength) // 10
print(triangleAndSquare.triangle.sideLength) // 10
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.square.sideLength) // 50
print(triangleAndSquare.triangle.sideLength) // 50

// 使用 optional 物件

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
// optional 物件使用時必須加入 ? 若物件本身為 nil 則後方表達示將會直接忽略並直接回傳 nil
let sideLength = optionalSquare?.sideLength

--

// enum

// 若使用 Int 類型則 enum 的 RawValue 會預設由 0 開始自動遞增
// 其他類型則必須指定各元素的 RawValue
enum Rank: Int {
    // 若第一個元素設定為 1 則會從 1 開始遞增
    case ace = 1
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, queen, king
    // 亦可宣告方法在 enum 內
    func simpleDescription() -> String {
        switch self {
        // 注意此處需在前方加入 . 符號
        // 因為是在 switch self 的 self 為 Rank 物件 所以下方的 case 可以直接使用 .ace 取得值
        // 若在 switch 的目標不是 enum 物件本身(或其元素) 則必須使用 Rank.ace
        case .ace:
            return "ace"
        case .jack:
            return "jack"
        case .queen:
            return "queen"
        case .king:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
print( ace.simpleDescription() ); // 字串結果的 ace

// 亦可使用以下方式取得 enum 元素

Rank(rawValue: 3)

// 搭配前面學到的 optional 方法可避免 rawValue 不存在的情況

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}

// 進階應用
// 使用 enum 搭配 struct 簡化程式碼

var result: Double = 0.0

enum Operation{
    // 可傳遞 associate value 以便之後取得 需注意此參數與 rawValue 是不同的意義 類似於 optional 的 associate value
    case Constant(Double)
    case UnaryOperation
}

var operations: Dictionary<String, Operations> = [
    "pi" : .Constant(M_PI)
    "e" : .Constant(M_E)
]

func performOperation(symbol: String){
    if( let operation == operations[symbol] ){
        switch operation {
            // 使用 let value 取得 associate value
            case .Constant(let value): 
                result = value;
            default: break;
        }
    }
}

--

// Struct
// 與 class 非常類似 亦可包含方法與欄位 僅可繼承 Protocal
// 但在作為參數傳遞時 struct 將會複製一份自己作為傳輸 而 class 則是僅傳遞記憶體位置
// 因此 struct 傳遞後會有多個物件存在於不同記憶體位置
// 而 class 則只會有一個記憶體位置

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}

--

// Protocal
// 類似於 Java 的 Interface

protocol ExampleProtocol {
    var simpleDescription: String { get }
    // mutating 表示此方法可能會在方法內修改 struct/enum 的屬性值
    mutating func adjust()
}

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    // 此處覆寫的方法前方不需加入 mutating 保留字 因為 class 本身原本就可以覆寫自己的屬性值
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    // Struct 則必須加上 mutating 保留字 以便覆寫屬性值
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}

--

// Extension
// 可以擴充原本存在的 Class 功能

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}

print(7.simpleDescription) // The number 7

--

// 多型
// 與 Java 相同 亦可直接將變數類型指定為 Protocal
// 可使用的方法只有 Protocal 內定義的方法

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)

--

// 錯誤處理

// 直接繼承 Error Protocal
enum PrinterError: Error {
    case outOfPaper
    case noToner
    case onFire
}

// 在會拋出異常的方法參數後方加入 throws 保留字 表示此方法會拋出異常
func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

// 使用 do-catch 控制會拋出異常的流程
// 須注意使用時在會拋出的方法前方需加入 try 保留字
do {
    let printerResponse = try send(job: 1040, toPrinter: "Never Has Toner")
    print(printerResponse)
} 
catch {
    print(error)
}

// 亦可使用多個 catch 處理不同的異常類型

do {
    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
    print(printerResponse)
} 
// 處理 PrinterError.onFire 這項錯誤
catch PrinterError.onFire {
    print("I'll just put this over here, with the rest of the fire.")
} 
// 處理所有 PrinterError 類型的錯誤
catch let printerError as PrinterError {
    print("Printer error: \(printerError).")
} 
// 處理其他所有錯誤
catch {
    print(error)
}

// 或是使用 optional 方式處理異常
// 會拋出異常的方法將回傳 nil

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner") // nil

--

// defer
// 類似於 Java 的 finally
// 會在 function 的所有內容執行完成後進入

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
 
func fridgeContains(_ food: String) -> Bool {
    fridgeIsOpen = true
    defer {
        fridgeIsOpen = false
    }
    
    let result = fridgeContent.contains(food)
    return result
}

fridgeContains("banana")
print(fridgeIsOpen) // false

--

// 泛型

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result = [Item]()
    // 此處使用 _ ,因為實際是不需使用到此變數 所以使用 _ 直接忽略變數宣告 類似於參數傳遞的 _ 標籤
    for _ in 0..<numberOfTimes {
        result.append(item)
    }
    return result
}
makeArray(repeating: "knock", numberOfTimes:4) // ["knock", "knock", "knock", "knock"]

--

// where
// 可加在 function 後方表示僅有符合此條件者才可進入 function 內
// 例如 在定義幾乎完全相同的 function 時( 如名稱、回傳類型相同 且參數皆為泛型時 )
// 在 Java 內會出現無法識別方法的編譯錯誤
// 而 Swift 則可以使用 where 制定不同泛型類型對應的不同處理方法

// Sequence 表示陣列類型的父類別
// where 表示 T, U 皆需實作 == 比對的方法(Equatable) 且兩者內容元素的類型相同才可進入此方法
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Int
    where T.Iterator.Element: Equatable, T.Iterator.Element == U.Iterator.Element {
        for lhsItem in lhs {
            for rhsItem in rhs {
                if lhsItem == rhsItem {
                    return 0
                }
            }
        }
        return 1
}

// 回傳為 Int 類型的相同泛型參數方法
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Int {
    return 2
}

// 回傳為 Bool 類型的相同泛型參數方法
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool {
    return false
}

// 以下三者將各自呼叫不同的方法
anyCommonElements([1, 2, 3], [3])   // 0
var a:Int = anyCommonElements([1, 2, 3], ["String"]) // 2
var b:Bool = anyCommonElements([1, 2, 3], ["String"]) // false

--

// 執行緒

let queue:OperationQueue = OperationQueue();
        
queue.addOperation {
    // Code
}

--

// lazy 保留字
// 會在真正使用到此變數時才進行初始化動作(僅能使用在 var 變數 不可使用在 let 常數)

lazy var calculator = Calculator() // not initialized yet

// 若要在給訂初始值時使用到 self 物件 可以使用 lazy 保留字
// 若未加入 lazy 則會出現 self 尚未被初始化錯誤

lazy var t: Type = {
    var r = self.someProperty * 2
    return r
}

--

// AnyObject
// 如字面意義 代表任何物件型態

var any:AnyObject;

// 是否為 SomeObjecy 類型
if any is SomeObject{

}

// 將值指定給 foo 前確認型態(Casting)
if let foo = any as? SomeObject{

}

// Assert
// Debug 時使用 在 release 時會自動忽略

// autoclosure 使用時不需加入 {}
assert( () -> Bool, "message")

assert( validate(), "Validate failed" )

各項資料連結
Swift Tour

Android - N(API 24) 的 FileUriExposedException

2016-10-01 10:43 AM

由於 Android N 加入了 intent 開啟檔案的權限判斷

所有使用 file:// 開頭的 uri 將會出現 FileUriExposedException

必須改為使用 content:// 開頭的 uri

也就是會用到 FileProvider

以下將以點擊通知開啟外部檔案為範例說明須修改的部分

首先在 res/xml 內建立一個 file_paths.xml 的檔案(名稱自訂)

程式碼範例
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="folderName" path="."/>
</paths>

// external-path: 等同於 Environment.getExternalStorageDirectory()
// name="folderName": 任意名稱 點擊後顯示的路徑資料夾名稱
// path=".": 實際要分享權限的資料夾路徑 使用 "." 代表根目錄

建立完成後再到 AndroidManifest.xml 加入 porvider 節點

<application>
...

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="your.package.name.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
    <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
</provider>

...
</application>

接著建立 Uri Intent

Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

File file = new File(dir, filename);

// API 24 之前使用的方式 會出現 FileUriExposedException
// Uri.fromFile(file)

// API 24 使用的方式
Uri uri = FileProvider.getUriForFile(getActivity(), "your.package.name.fileprovider", file);
intent.setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("html"));

int flags = PendingIntent.FLAG_UPDATE_CURRENT;
PendingIntent pIntent = PendingIntent.getActivity(getActivity(), REQEUST_CODE, intent, flags);

// 取得系統的通知服務
NotificationManagerCompat notificationManager = NotificationManagerCompat.from( getActivity() );

// 建立通知
final Notification notification = new NotificationCompat.Builder( getActivity() )
        .setSmallIcon( R.drawable.ic_notification )
        .setLargeIcon( BitmapFactory.decodeResource(getResources(), R.drawable.ic_notification_large) )
        .setContentTitle( notificationTitle )
        .setContentText( filename )
        .setContentIntent(pIntent)
        .setOngoing(false)
        .setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS)
        .setPriority(Notification.PRIORITY_MAX)
        .build();

// 發送通知
notificationManager.notify(NID, notification);
各項資料連結
Android - FileProvider

Android - VISIBLE INVISIBLE 及 GONE 的差異

2016-06-09 4:24 PM
Android 的 View 有三種可視狀態

VISIBLE, INVISIBLE 以及 GONE

這三種的差異在哪呢?

以下將針對差異的部分做一個簡單又稍微有點深入的說明

應該能在各位設定可視狀態時做一個參考

第一點:看得見與看不見的差異

當 View 被設定為 VISIBLE 就表示他是可見的,

在父層級(也就是 ViewGroup)在做 onLayout 與 onMeasure 動作時都會呼叫子 View 的 onLayout 與 onMeasure 動作

也會做所有的 onDraw 動作

這就表示會在螢幕上佔據一個空間

並將與其相對的所有 View 做一個相對位置(若有的話)的運算

然後將所有的變化經由 onDraw 畫出來

當 View 被設定為 INVISIBLE 時表示他是不可見的。

也同樣會做 onLayout 與 onMeasure 的動作

但不會做 onDraw 的動作

因此雖然會在螢幕上佔據一個空間並保有與其他 View 的相對位置

但看不見任何東西在螢幕上面

若 View 設定為 GONE 也同樣表示不可見

但此同時他也不會做任何的 onLayout 、 onMeasure 以及 onDraw 的動作

因此他不會具有相對位置的計算

也不會佔有任何空間

第二點:效能上的差異

在螢幕大小改變或是初始化時 Android 都會呼叫各個 View、ViewGroup 做 onMeasure 及 onLayout 的動作

若沒有這些情況發生,通常只會做一輪這個動作(其中同一個 View 的 onMeasure 會呼叫非常非常多次 但 onLayout 次數通常只有一次)

如果發生了螢幕大小改變(例如設定鍵盤出現時將畫面縮小)

就會重新做一輪這個動作(有時 onLayout 會做兩次或以上)

因此請將這個 onMeasure 及 onLayout 行為當作非常耗時的動作並謹慎看待(事實上也是)

並仔細設計 Layout 的結構

讓這些動作運算的時間盡量少一點

或是將不必要的 View 設定為 Gone 而略過這些動作

而重點就在這裡

如果你有一個 View 經常在看得見與看不見之間來回切換

這時你會選擇設定為 INVISIBLE 還是 GONE?

有人會問說 這有什麼差別?

事實上差別可大了

若你選擇使用 GONE

那麼這個 View 會在可視狀態切換時重新呼叫 onMeasure 以及 onLayout

並且是從這個 View 的父層級開始呼叫(往上幾層倒是不確定)

也就是說假如你有一個 ViewGroup 裡面包含了十個 View

那麼當其中一個 View 在 VISIBLE 跟 GONE 之間切換時

將會呼叫 ViewGroup 及所有 View 的 onMeasure 及 onLayout

因此這時如果有一個動畫的展示

很可能會造成這個動畫有 lag 的情形發生

換言之若選擇使用 INVISIBLE

那麼在可視狀態切換時並不會重新呼叫這一大堆的 onMeasure 及 onLayout

也就是說如果在這時有動畫的展示

看起還會比使用 GONE 切換來得順暢一點

有人就會覺得 那乾脆全部用 INVISIBLE 不就好了?

但就像前面提到的

如果螢幕大小發生變化 INVISIBLE 的 View 也同樣需要重新 onMeasure 及 onLayout

反而這時使用 GONE 會是比較好的選擇

因此在這兩者之間要如何斟酌就要看使用者自己的評估了

如果是常常在狀態間切換用的 View 其實可以設定為 INVISIBLE 狀態

反之如果是偶爾才出現一次的 View 就可以設定為 GONE 來略過不必要的 onMeasure 及 onLayout 過程

Android - KitKat 以上版本從 URI 取得檔案實體路徑

2016-05-26 9:42 AM

有時使用檔案選擇器取得路徑時會發現路徑是奇怪的 content://downloads/public_downloads 字串

而這種路徑使用 context.getContentResolver().query(); 方法取得的路徑會是空的

其實是因為新版 Android 在路徑上是使用 uri 的方式表示的

有點像網址,好處是同一個 uri 可以指定到不同的實體路徑內

用以對應不同的開發商與使用者設定

壞處就是取得路徑的方法變得較麻煩(而且官方文件似乎找不到方法? WTF?)

而且很多類型的檔案取得路徑的方式也沒有統一

以下就是對應各版本(4.4 以上)取得路徑的 FileUtil 的原始碼

檔案瀏覽器的使用方式可以到 啟動檔案瀏覽器選擇檔案 教學查看

程式碼範例
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import cmc.toolkit.FileKit;
import com.ihad.ptt.model.exception.EmptyFilePathException;
import com.ihad.ptt.model.exception.IllegalTrueTypeFontException;

import java.io.File;
import java.io.FileNotFoundException;

public class FileUtils {

    /**
     * 從 uri 取得檔案路徑 這將從 Storage Access Framework Documents 取得路徑
     * 也會使用從 _data 欄位取得 MediaStore 以及其他 file-based ContentProviders 的對應路徑
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @author paulburke
     */
    public static String getPath(final Context context, final Uri uri) {

        // DocumentProvider
        if ( DocumentsContract.isDocumentUri(context, uri) ) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    String path = Environment.getExternalStorageDirectory().getPath();

                    if (split.length > 1) {
                        path += "/" + split[1];
                    }

                    return path;
                }
                else {
                    String path;
                    if (Environment.isExternalStorageRemovable()) {
                        path = System.getenv("EXTERNAL_STORAGE");
                    } else {
                        path = System.getenv("SECONDARY_STORAGE");
                        if (path == null || path.length() == 0) {
                            path = System.getenv("EXTERNAL_SDCARD_STORAGE");
                        }
                    }

                    if (split.length > 1) {
                        path += "/" + split[1];
                    }

                    return path;
                }
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[] {
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            // Return the remote address
            if (isGooglePhotosUri(uri)) return uri.getLastPathSegment();

            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    /**
     * 從 data 欄位取得 uri 對應的實體路徑
     * 主要用以取得 MediaStore Uris 以及其他 file-based ContentProviders 的對應路徑
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @param selection (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     * @author paulburke
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
                                       String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }


    /**
     * @param uri The Uri to check.
     * @return 是否為 ExternalStorageProvider 類型的 uri
     * @author paulburke
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return 是否為 DownloadsProvider 類型的 uri
     * @author paulburke
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return MediaProvider 類型的 uri
     * @author paulburke
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return 是否為 Google Photos 類型的 uri
     */
    public static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }

    // 檢查檔案類型的小範例

    /**
     * 檢查取回的檔案副檔名
     * @param path  檔案路徑
     * @return
     *  若檔案類型正確則回傳檔案名稱, 反之則回傳空字串
     * @throws EmptyFilePathException 路徑是空字串或是 null 物件
     * @throws FileNotFoundException 檔案不存在
     * @throws IllegalTrueTypeFontException 檔案類型不正確
     */
    public static String typefaceChecker(String path) throws EmptyFilePathException, FileNotFoundException, IllegalTrueTypeFontException {
        if( path == null || path.isEmpty() ){
            throw new EmptyFilePathException( "File path is empty" );
        }

        File file = new File(path);

        if( !file.exists() || !file.isFile() ) {
            throw new FileNotFoundException( "File path : " + path );
        }

        String filename = file.getName();
        String ext      = FileKit.getFileExt(filename);

        if( !ext.equalsIgnoreCase(".ttf") ){
            throw new IllegalTrueTypeFontException( "File path : " + path + ", File name : " + filename + ", ext : " + ext );
        }

        return filename;
    }
}
各項資料連結
啟動檔案瀏覽器選擇檔案
Stackoverflow - Get real path from URI, Android KitKat new storage access framework
GitHub - aFileChooser

Android - 自訂 ViewGroup 以及 CustomView 的文字大小設定

2016-05-25 7:28 PM

在 CustomViewGroup 或 CustomView 內設置文字大小時

若直接取得 Dimension 再指定給文字顯示物件

會發現字體變得非常的大

// 使用 dimension
textSize = a.getDimension(R.styleable.StackedTextView_line1_textSize, 0);

textView.setTextSize(textSize);

其實是因為使用以上方式取得的大小設定其實是已經轉換過的大小

因此我們只要取得原始的大小設定再套用解析度設定即可

// 使用 dimensionPixelSize
textSize = a.getDimensionPixelSize(R.styleable.StackedTextView_line1_textSize, 0);

// 加入 TypedValue.COMPLEX_UNIT_PX 的設定
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
各項資料連結
Android Developers

Andoird - 啟動檔案瀏覽器選擇檔案

2015-12-31 11:16 AM

App 開發真的比 Web 還來得辛苦啊...

抱怨結束

在開發時常常需要有選擇圖片、檔案等等的功能

這時就需要啟動外部 App (啟動檔案瀏覽器) 來獲得選取檔案後的結果

例如點擊按鈕後啟動檔案管理員,選擇檔案後取得檔案路徑

再使用檔案路徑完成上傳檔案啦~ 圖片編輯等等的後續處理

搜尋 Intent file browser 可以得到很多資訊喔

接下來就開始介紹如何實現這項功能

4.4 版本以上由於 Android 修改了檔案的存取方式

請搭配這篇教學的方法使用 KitKat 以上版本從 URI 取得檔案實體路徑

程式碼範例
// 外部 App 回傳結果的類型判斷碼
private static final int FILE_SELECT_CODE = 0;

// 在按鈕上監聽點擊事件
settingsTypefaceButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        fileBrowserIntent();
    }
});

/**
 * 啟動外部 App 的檔案管理員
 */
private void fileBrowserIntent(){
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    // 設定 MIME Type 但這裡是沒用的 加個心安而已
    intent.setType("application/font-sfnt");
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    try {
        startActivityForResult( Intent.createChooser(intent, "選擇字型"), FILE_SELECT_CODE );
    } catch (android.content.ActivityNotFoundException ex) {
        // 若使用者沒有安裝檔案瀏覽器的 App 則顯示提示訊息
        Toast.makeText(this, "沒有檔案瀏覽器 是沒辦法選擇字型的", Toast.LENGTH_SHORT).show();
    }
}

/**
 * 使用者選擇的結果將會作為參數傳遞至此
 * @param requestCode   發出要求的代碼, 用以確定是取得何種結果
 * @param resultCode    回傳結果的代碼, 用已確定是否請求成功
 * @param data          回傳的資料
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        // 檔案選擇代碼
        case FILE_SELECT_CODE:
            // 請求確認返回
            if (resultCode == RESULT_OK) {
                // 取得檔案路徑 Uri
                Uri uri = data.getData();
                // 取得路徑
                String path = null;
                try {
                    path = FileUtils.getPath(this, uri);
                } catch (URISyntaxException e) {
                    Toast.makeText(this, "檔案不對勁!", Toast.LENGTH_SHORT).show();
                    logger.error("Get file path failed.", e);
                    return;
                }

                // 檢查檔案類型
                String filename = FileUtils.typefaceChecker(path);

                if( filename.isEmpty() ){
                    Toast.makeText(this, "檔案不對勁!", Toast.LENGTH_SHORT).show();
                    return;
                }

                // Do something here...
            }
            break;
    }
    super.onActivityResult(requestCode, resultCode, data);
}

眼尖的人就注意到了,FileUtils 是啥? 為什麼我沒有!!!

為什麼網路教學老是缺東缺西的啊!!! 崩潰啊啊啊啊啊啊啊啊!!!

我說寧王先別急著發飆啊

因為那是需要自己建立的 Class

原始碼如下

import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import cmc.toolkit.FileKit;

import java.io.File;
import java.net.URISyntaxException;

public class FileUtils {

    /**
     * 由外部 Activity 回傳的資料取得檔案路徑
     * @param context   Activity
     * @param uri       Uri
     * @return
     *  檔案路徑
     * @throws URISyntaxException
     */
    public static String getPath(Context context, Uri uri) throws URISyntaxException {
        if ("content".equalsIgnoreCase(uri.getScheme())) {
            String[] projection = { "_data" };
            Cursor cursor = null;

            try {
                cursor = context.getContentResolver().query(uri, projection, null, null, null);
                int column_index = cursor.getColumnIndexOrThrow("_data");
                if (cursor.moveToFirst()) {
                    return cursor.getString(column_index);
                }
            } catch (Exception e) {
                // Do nothing
            }
            finally {
                if( cursor != null ) cursor.close();
            }
        }
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    /**
     * 檢查取回的檔案副檔名
     * @param path  檔案路徑
     * @return
     *  若檔案類型正確則回傳檔案名稱, 反之則回傳空字串
     */
    public static String typefaceChecker(String path){
        if( path == null || path.isEmpty() ) return "";

        File file = new File(path);

        if( !file.exists() || !file.isFile() ) {
            return "";
        }

        String filename = file.getName();
        String ext      = FileKit.getFileExt(filename);

        if( !ext.equalsIgnoreCase(".ttf") ){
            return "";
        }

        return filename;
    }

}



// 一併附上取得副檔名的程式碼, FileKit 是我剛學 Java 時自己寫的小工具

public static String getFileExt(String fileName) {
    int dotIndex = fileName.lastIndexOf(".");
    return dotIndex == -1 ? "" : fileName.substring(dotIndex);
}
各項資料連結
Stackoverflow - Android file chooser

Java - PTT 文章代碼(AID)轉換為 URL

2015-12-26 5:38 PM

因為開發經驗第二集要花比較多時間撰寫

剛好最近有用到 AID(文章代碼) 轉換成 URL 網址的方法

寫了一個轉換工具 AidConverter

可以轉換 AID 為 URL 或是 檔案名稱

也可以將 URL 或 檔案名稱 轉換為 AID

就決定先寫這篇教學文章了

PTT 的原始碼是用 C 語言寫的 而且檔名很可怕!!!

但我只碰過 C++ 而且那也已經是N年前的事了(遙想當年...)

我懶得複習也懶得去看那幾段程式碼到底在做什麼

就用硬轉的方式把那幾段轉成 Java 語言

所以這段教學不會有什麼說明 打算放放原始碼就了事...

雖然測試回傳結果都是正確的 但只測過十幾篇所以記得自己加一些錯誤處理

如果有大師願意修正為更好的版本也請不要客氣囉~

AidConverter
import com.google.common.base.Splitter;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.ihad.ptt.model.bean.AidBean;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

/**
 * 文章編號與檔案名稱轉換工具<br />
 * 將符合此格式的 [a-zA-Z-_]{8} 文章編號轉換為檔案名稱 或是 URL<br />
 * 範例:<br />
 * 1MVYyFDv -> M.1451110159.A.379 -> https://www.ptt.cc/bbs/Gossiping/M.1451110159.A.379.html<br /><br />
 * 或是將符合 [M|G].[unsigned_integer].A.[HEX{3}] 格式的檔案名稱轉換為文章編號<br />
 * 若為 URL 則將轉換為 {@link AidBean}, 其中包含看板名與文章編號<br />
 * 範例:<br />
 * https://www.ptt.cc/bbs/Gossiping/M.1451110159.A.379.html -> M.1451110159.A.379 -> 1MVYyFDv
 */
public class AidConverter {

    private static final String DOMAIN_URL = "https://www.ptt.cc/bbs/";
    private static final String FILE_EXT = ".html";

    private static String aidTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
    private static BiMap<Character, Long> table = tableInitializer();

    /**
     * 建立文章編號字元 Map, 方便取得對應數值
     * @return
     *  文章編號字元 Map
     */
    private static BiMap<Character, Long> tableInitializer(){
        BiMap<Character, Long> table = HashBiMap.create();

        long index = 0;
        int size = aidTable.length();

        for( int i = 0; i < size; i++ ){
            table.forcePut( aidTable.charAt(i), index );
            index ++;
        }

        return table;
    }

    /**
     * 將檔案名稱轉換為˙數字型態的文章序號
     * @param fn    檔案名稱
     * @return
     *  數字型態的文章序號, 若檔案名稱格式不符將回傳 0<br />
     *  轉換後的文章編號將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
     *  範例: M.1451100858.A.71E
     */
    private static long fn2aidu(String fn){
        long aidu = 0;
        long type = 0;
        long v1 = 0;
        long v2 = 0;

        if( fn == null ) return 0;

        List<String> fnList = Splitter.on(".").omitEmptyStrings().trimResults().splitToList(fn);

        if( fnList.size() != 4 ) return 0;

        String typeString = fnList.get(0);
        String v1String = fnList.get(1);
        String v2String = fnList.get(3);

        if( !fnList.get(2).equals("A") ) return 0;
        if( !StringUtils.isNumeric(v1String) || v1String.length() != 10 ) return 0;

        switch (typeString){
            case "M":
                type = 0;
                break;
            case "G":
                type = 1;
                break;
            default:
                return 0;
        }

        v1 = Long.parseLong( v1String );
        v2 = Long.parseLong( v2String, 16 );

        aidu = ((type & 0xf) << 44) | ((v1 & 0xffffffffL) << 12) | (v2 & 0xfff);

        return aidu;
    }

    /**
     * 將數字型態的文章序號轉換為字串型態的文章編號
     * @param aidu  數字型態之文章序號
     * @return
     *  轉換後的文章編號將符合 [a-zA-Z-_]{8}<br />
     *  範例: 1MVWgwSU
     */
    private static String aidu2aidc(long aidu){
        int size = table.size();
        BiMap<Long, Character> inverseTable = table.inverse();

        StringBuffer stringBuffer = new StringBuffer();

        while( stringBuffer.length() < 8 ){
            long v = aidu % size;
            if( !inverseTable.containsKey( v ) ) return null;

            stringBuffer.insert( 0, inverseTable.get( v ) );
            aidu = aidu / size;
        }

        return stringBuffer.toString();
    }

    /**
     * 將文章編號轉換為數字型態的文章序號
     * @param aid   文章編號
     * @return
     *  數字型態的文章序號
     */
    private static long aidc2aidu(String aid){
        char[] aidChars = aid.toCharArray();
        long aidu = 0;

        for( char aidChar : aidChars ){
            if( aidChar == '@' ) break;
            if( !table.containsKey(aidChar) ) return 0;

            long v = table.get(aidChar);

            aidu = aidu << 6;
            aidu = aidu | (v & 0x3f);
        }

        return aidu;
    }

    /**
     * 將文章序號(數字型態)轉換為檔案名稱
     * @param aidu  文章序號(數字型態)
     * @return
     *  轉換後的檔案名稱, 格式將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
     *  最後的16進位表示法若未滿3個字將以0從左邊開始補齊<br />
     *  範例: M.1451100858.A.71E
     */
    private static String aidu2fn(long aidu){
        long type = ((aidu >> 44) & 0xf);
        long v1 = ((aidu >> 12) & 0xffffffffL);
        long v2 = (aidu & 0xfff);

        // casting to unsigned
//        v1 = v1 & 0xffffffffL;
        String hex = Long.toHexString(v2).toUpperCase();

        return ((type == 0) ? "M" : "G") + "." + v1 + ".A." + StringUtils.leftPad(hex, 3, "0");
    }

    /**
     * 將文章編號轉換為檔案名稱
     * @param aid   文章編號
     * @return
     *  轉換後的檔案名稱, 格式將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
     *  最後的16進位表示法若未滿3個字將以0從左邊開始補齊<br />
     *  範例: M.1451100858.A.71E
     */
    public static String aidToFileName(String aid){
        return aidu2fn( aidc2aidu(aid) );
    }

    /**
     * 將文章編號轉換為 WEB 版 URL
     * @param boardTitle    文章所屬看板名稱
     * @param aid           文章編號
     * @return
     *  WEB 版的完整 URL
     */
    public static String aidToUrl(String boardTitle, String aid){
        if( boardTitle.isEmpty() || aid.isEmpty() ) return "";

        return DOMAIN_URL + boardTitle + "/" + aidToFileName(aid) + FILE_EXT;
    }

    /**
     * 將檔案名稱(也就是 URL 的最後一段 不包含副檔名)轉換為文章編號
     * @param fileName  檔案名稱
     * @return
     *  轉換後的文章編號, 若檔案名稱格式不符則將回傳 null
     */
    public static String fileNameToAid(String fileName){
        return aidu2aidc( fn2aidu(fileName) );
    }

    /**
     * 將 URL 轉換為 AID 物件
     * @param url   PTT WEB 版的 URL
     * @return
     *  物件內包含 文章編號 與 看板名
     * @see AidBean
     */
    public static AidBean urlToAid(String url){
        List<String> urlList = Splitter.on("/").omitEmptyStrings().trimResults().splitToList(url);

        if( urlList.size() < 4 ) return null;

        String boardTitle   = urlList.get(3);
        String fileName     = urlList.get(4).replaceAll("\\.(html|htm)", "");
        String aid          = fileNameToAid( fileName );

        return AidBean.Builder.anAidBean()
                .withBoardTitle( boardTitle )
                .withAid( aid )
                .build();
    }

}

AidBean
public class AidBean{

    // 看板名稱
    private String boardTitle;

    // 文章編號
    private String aid;

    public String getBoardTitle() {
        return boardTitle;
    }

    public void setBoardTitle(String boardTitle) {
        this.boardTitle = boardTitle;
    }

    public String getAid() {
        return aid;
    }

    public void setAid(String aid) {
        this.aid = aid;
    }

    public static class Builder {
        private String boardTitle;
        private String aid;

        private Builder() {
        }

        public static Builder anAidBean() {
            return new Builder();
        }

        public Builder withBoardTitle(String boardTitle) {
            this.boardTitle = boardTitle;
            return this;
        }

        public Builder withAid(String aid) {
            this.aid = aid;
            return this;
        }

        public Builder but() {
            return anAidBean().withBoardTitle(boardTitle).withAid(aid);
        }

        public AidBean build() {
            AidBean aidBean = new AidBean();
            aidBean.setBoardTitle(boardTitle);
            aidBean.setAid(aid);
            return aidBean;
        }
    }
}
各項資料連結
Java - PTT 批踢踢實業坊 Client 軟體開發經驗(1) - 前置準備
PTT 原始碼

Java - PTT Client 軟體開發經驗(1) - 前置準備

2015-12-16 11:41 AM

警告! 開發 PTT Client 非常之危險!

無所不在的時間黑洞以及各種無法預料的狀況會將你的精神折磨至崩潰!!!

哈哈哈哈哈哈哈哈哈哈(就像這樣)

--

以下都是廢話加閒談,趕時間的可以按這裡跳到重點

可喜可賀!

經過了漫長的開發時間終於成功上架了

目前似乎也運氣很好的穩定運行了一段時間

有興趣的朋友可以自己在部落格內尋找 App 連結,我就不多打廣告了

自信滿滿,人稱程式小Bug王的我原本想說這種小東西大概一兩個月就可以完成了吧!

結果花了我快半年...

其中一個原因是第一次開發 Android App

另一個就是氣死人不償命的 VT100 終端機訊息傳遞機制,這個留待往後慢慢說明

而且網路上的資源可以說是少之又少

很多東西都是自己Google來東拼西湊才知道的

所以很多東西看看就好不一定是對的,自己找重點記(超不負責任)

尤其很多都是英文的資料,對於 PTT 這中文的論壇所需的技術來說實在是很不友善

於是我就想說寫這篇教學文章來好好闡述一下這血與淚的開發史

希望可以求得一些歷史定位(!?)

呃...是造福其他開發者啦

最主要的目的是告訴其他有興趣的開發者趕快放棄開發的計劃,因為實在太難做了

如果再讓我選一次我絕對不會選擇開發這個 App!!!

心理準備

首先我們來彈彈 PTT 的額頭,要很用力!(到底有多恨...)

是來談談 PTT 使用的 ANSI Escape Sequence 以及相關的知識

如果以為這只是單純的文字處理那就大錯特錯了!

當初天真爛漫的我其實就是這麼想的,殊不知背後龐大的利益鳩葛與情仇交纏...

PTT 是使用 VT100 這種類型的終端機定義來傳遞訊息

而使用 ANSI Escape Sequence 則可以將訊息格式化

諸如文字顏色、背景顏色、閃爍、文字出現位置以及清除畫面等等

所以你用 PC MAN 或任何終端機連上後可以看得見如此精美的畫面


但是你得到的資料其實是...

 [H [1;37;44m【主功能表】                      [33m批踢踢實業坊 [0;1;37;44m                                  
 [0;47m        [30m│ [2;6H                                                      [0;47m﹎ [30m﹏︷~一︸一~︷﹏﹎ [0;47m  
     [30;43m╱ [3;3H     [31;47m◢ [3;5H [41m      [30m﹏     ╱ [3;15H [47m           [1m▄ [3;23H    ▄ [3;25H    ▄ [3;27H    ▄ [3;29H    ▄ [3;31H    ▄ [3;33H    ▄ [3;35H    ▄ [3;37H    ▄ [3;39H    ▄ [3;41H                       [0;30;47m∕ [3;61H明天要出去玩,  ﹨  
 [0;43m   [41m  [30m#          ╱ [4;13H   [47m          [0;47m█ [4;22H [0;1;30m  [34m/  [0;1;37m.       [5;34m∕ [4;30H     [0;30m█ [4;32H [1;36m/   [5;34m/ [0;5;34m      [0;1;30m║ [4;40H    █ [4;42H [0;47m           ▁ [4;51H     [30m▂ [4;53H    ▁ [4;55H [0;47m     [30m﹨   雨還沒停嗎...    ∕ [4;79H  
     [0;43m◢ [5;3H [33;41m     [30m〞    ╱ [5;11H _   [47m          [0;47m█ [5;22H [0;1;30m   [5;36m/ [0;5;36m  [0;1;5;37m.      [0;1;36m∕ [5;31H     [30m█ [5;33H [0;30m  [0;1;37m.     ● [5;38H [30m人    █ [5;42H [0;47m          [30m◢ [5;49H [0;30m           [47m◣ [5;57H [0;47m  ﹊ [30m`"︸一~︷~一︸"`﹊ [0;47m  
     [43m◤ [6;3H [33;41m      [30m氵 [6;6H     ╱ [6;9H     ╱ [6;12H    ║ [6;14H  [47m          [0;47m█ [6;22H [0;1;30m      [34m∕ [6;25H     [36m′ [6;27H [0;36m       [30m▃ [6;31H     [1m█ [6;33H     [5;34m′ [6;35H [36m/      [0;1;37m▲ [6;39H      [30m█ [6;42H [0;47m          [30m◤ [6;49H     [43m◤ [6;51H    ◤ [6;53H [0;30m        [47m▏ [6;58H      ∞ [6;62H [0;47m         [36m▃ [6;68H    ▄ [6;70H    ▃ [6;72H [0;47m         
     [33;41m◤ [7;3H       [30m╱ [7;7H    ▏ [7;9H/      ║ [7;14H  [47m          [0;47m█ [7;22H [0;1;30m     [5;34m/ [0;1;36m/     [0;30m◤ [7;31H     [1m█ [7;33H [0;30m  [1;34m/     ′ [7;38H [0;34m  [1;5m/     [0;1;30m█ [7;42H [0;47m       [43m      [30m˙ [7;51H      [33m◤ [7;54H     [30m◥ [7;56H     [47m▎ [7;58H。           [36;46m◤ [7;68H [1;33;47m/|\          
 [0;30;41m`     ╱ [8;5H      [31m▋ [8;8H     [30m║ [8;10H      ║ [8;14H  [47m          [0;47m█ [8;22H [0;1;30m      [36m′ [8;25H [0;36m     [1;34m/     [30m█ [8;33H [0;30m   [1;36m/ [0;36m  [0;1;5;37m.      [0;1;30m█ [8;42H [0;47m       [43m          [33m◢ [8;55H [0;30m      [47m▏ [8;58H         [46m  [47m              
     [41m╱ [9;3H _      [31m▋ [9;8H     [30m║ [9;10H      ║ [9;14H  [47m          [0;47m█ [9;22H     [0;1;5;36m∕ [9;24H     [0;1;34m′ [9;26H [5;36m/ [0;5;36m  [0;1;37m.      [30m█ [9;33H     [34m∕ [9;35H     [5;36m′ [9;37H [0;5;36m      [0;1;36m∕ [9;40H     [30m█ [9;42H [0;47m         ◥ [9;49H [31;43mㄟ [33m      ▲ [9;55H     [30;47m◤ [9;57H [0;47m          [46m  [47m                [1;30mξ [9;80H 
 [0;41m      [30m╱ [10;4H    ║ [10;6H     [31m▋ [10;8H     [30m║ [10;10H      ║ [10;14H  [47m          [0;47m█ [10;22H     [0;1;30m▄ [10;24H    ▄ [10;26H    ▄ [10;28H    ▄ [10;30H    ▄ [10;32H    ▄ [10;34H    ▄ [10;36H    ▄ [10;38H    ▄ [10;40H    █ [10;42H [0;47m              [30;42m╭ [10;53H    ╮ [10;55H [47m             [33m◢ [10;65H     [36;43m▃ [10;67H    ▃ [10;69H [33m       [31m▁ [10;73H    ▁ [10;75H       [33m▲ [10;79H     [44m▲ [10;81H
 [30;41m/      ║ [11;6H     [31m▋ [11;8H     [30m║ [11;10H      ║ [11;14H      [1;47mψ [11;17Hrod24574575                            [0;30;42m│ [11;53H    │ [11;55H [47m           [33m◢ [11;63H [43m           [31m▕ [11;71H     [30;47m≡ [11;73H     [1m▏ [11;75H     [0;31;43m▏ [11;77H  [34m ̄ 
 [m [K
    ─ [13;3H    ─ [13;5H    ─ [13;7H    ─ [13;9H    ─ [13;11H    ─ [13;13H    ─ [13;15H    ─ [13;17H    ─ [13;19H 上方為使用者心情點播留言區,不代表本站立場     ─ [13;65H    ─ [13;67H    ─ [13;69H    ─ [13;71H    ─ [13;73H    ─ [13;75H    ─ [13;77H    ─ [13;79H [14;10H             ( [1;36mA [m)nnounce     【 精華公佈欄 】 [K [15;10H             ( [1;36mF [m)avorite     【 我 的 最愛 】 [K [16;10H             ( [1;36mC [m)lass        【 分組討論區 】 [K [17;10H             ( [1;36mM [m)ail         【 私人信件區 】 [K [18;10H             ( [1;36mT [m)alk         【 休閒聊天區 】 [K [19;10H             ( [1;36mU [m)ser [19;38H【 個人設定區 】 [K [20;10H             ( [1;36mX [m)yz          【 系統資訊區 】 [K [21;10H             ( [1;36mP [m)lay         【 娛樂與休閒 】 [K [22;10H             ( [1;36mN [m)amelist     【 編特別名單 】 [K [23;10H           > ( [1;36mG [m)oodbye         離開,再見    … [23;53H [K
 [34;46m[12/16 星期三 12:04] [1;33;45m [ 天秤時 ]    [24;36H [30;47m線上 [31m108017 [30m人, 我是 [31mnoartsarc [30m    [呼叫器] [31m打開  [m [23;21H

有沒有感覺到一種被欺騙了的感覺

...

...

...

那麼接下來我們就進入正式的介紹了(現在才開始!? 那前面打那麼多字幹嘛啦)

ANSI Escape Sequence

首先我希望各位將以下幾個網頁加入最愛或是直接儲存整個網頁

對於往後的開發來說會是非常重要的資源

控制字元對照表 - ASCII code caracters

ANSI 控制碼對照表 - ANSI Codes

CTRL 複合按鍵對照表 - Key Pressed with CTRL key down

第一個跟第三個連結暫時還用不上,我們先打開 ANSI Codes 的連結

然後看到下方表格的 color and text formatting 這欄


這邊主要是在說明所有可以用的文字格式設定控制碼

 /**
  * ESC 就是代表鍵盤上的 ESC 字元, 如何輸入留待後面再提
  * [#(;#) 中間的 # 就是表格內的數字組合, 可以放很多組格式設定
  * 但重複的設定會被後面的覆蓋掉
  * 例如我想要有紅色的字體, 綠色的背景再讓他閃爍就會使用以下控制碼
  * ESC[1;31;42m紅字綠背景ESC[m
  * 而如果重複使用了前景或背景設定
  * ESC[1;36;35;34;33;32;43;42m紅字綠背景ESC[m
  * 他則會以最後出現的設定來顯示(以上範例為32;42兩組設定)
  * 所以以上兩個範例顯示的結果會是相同的
  * 最後面的 ESC[m 則是還原用控制碼
  * 將所有文字格式設定還原
  * 而控制碼的結尾必須要以 m 告知處理器格式設定已經完成了
  * 這邊的觀念非常重要, 因為我們必須親自寫出一個上面提到的處理器
  * 也就是 ANSI Escape Sequence Parsing Engine
  */
 ESC[#(;#)m

再來就是另一個比較複雜的重頭戲了

請看到網頁中表格的 cursor controls 以及 erase functions


這邊主要是在處理文字的排版

也是上面那一大串文字的最主要原因

PTT 回傳的資料有很多種類型, 有時會給你完整的字串用空白補齊位子

有時會不給你空白, 而是使用 ESC[12;30H 的方式告訴你接下來的字是放在哪個位置

 /**
  * ESC 就是代表鍵盤上的 ESC 字元, 如何輸入留待後面再提
  * [#;#H 中間的 # 表示要將游標移動到哪個位置
  * 而這個游標位置又影響了接下來的字串會在哪個位置開始插入
  * 可以想像成一個 24 * 80 的矩陣會比較容易了解
  * 例如PTT回傳了要在 第 2 行的第 20 個的位置開始插入字串就會使用
  * ESC[2;20H隨便字串
  * 以此類推, 而若是使用
  * ESC[H
  * 則表示將游標位置歸零
  */
 ESC[#;#H

 // 清空所有資料
 ESC[2J
 // 清空目前所在行數的資料
 ESC[K

 // 將游標往後退一格 Backspace
 \b

 // 將游標回到同一列的第一個位置
 \r

 // 將游標往下移動一個 row
 \n

其他的部分因為 PTT 不會回傳那些控制碼所以可以暫時不用看

下一章就會開始講解 ANSI Escape Sequence Parsing Engine 該如何撰寫

用一長串英文有沒有感覺很專業?

其實就是將 PTT 回傳的資料一個蘿蔔一個坑的放入剛剛提到的 24 * 80 陣列的方法

算是整個 PTT Client 的第一個最重要的核心

也是當初撰寫時遇到的第一個難關

那麼就容我稍微拖個稿,請待下回分解!!!

各項資料連結
Java - PTT 文章代碼(AID)轉換為 URL
VT100
ASCII code caracters
ANSI Codes
Key Pressed with CTRL key down
PTT 完全操作手冊

PTT 行動版無廣告瀏覽器 - PiTT

2015-11-27 12:03 PM


全新無廣告行動版 PTT 瀏覽器登場囉! 用手機也可以輕鬆逛 PTT!

行動裝置上最懂你的 PTT 瀏覽器,心目中最理想的行動版 PTT 就該像這樣! I SEE YOU

立即前往 Google Play 下載!

讚不絕口的獨家特色

- 無廣告! 無廣告! 無廣告!
因為很重要所以說三次,
在此保證在功能 "做好做滿" 之前不會置入廣告!
置入廣告前的幾個版本更新會通知各位使用者,不更新就永遠不會有廣告!

- ANSI/MOBI 自由切換模式
看 ANSI ART 不需重新載入文章內容消耗行動網路流量,
讓你輕鬆看鄉民的藝術天分有多高!
ANSI 模式(其他 APP 稱為整頁模式) 可以等寬文字進行瀏覽
MOBI 模式則是以最佳化的行動版介面顯示

- 帳號任意門
其實全PTT只有我一個人,不信我換個帳號推一樣的話給你看。
不論是在最愛列表,文章列表還是文章內容,
你愛怎麼換帳號就怎麼換帳號。

- 推文進度報馬仔
打了一大堆推文卻不知到要推到何年何月?
獨家推文進度顯示告訴你還有幾則推文要繼續推
也告訴你還要等幾秒才可以繼續推
推到一半不想推還可以隨時中斷
直到你離開文章內容頁之前,推文暫存會為了你一直存在!
剩餘推文字數就不用說了,你懂的~

- 發文暫存功能
就算你關機了我也可以幫你回復你放棄編輯的文章!
文章編輯到一半突然想看八卦?
放心退出文章編輯模式去看吧!
我已經把你的文章內容都放在資料庫裡了!

- 發文 Redo/Undo 功能
複製貼上發現貼錯了...
刪除到不該刪的段落了...
乖,按一下 Redo/Undo 就通通幫你恢復原樣
比哆啦A夢的時光包巾還好用

- 文章瀏覽紀錄
忘記推了哪篇文章想看又找不到?
經典大長篇卻忘記標題是什麼?
不只記錄看板,你看過的文章通通幫你記錄下來
不用消耗行動網路流量也可以輕鬆追文

- 正/反向文章閱讀順序
文章太長想 End?
End 後發現是長篇經典文章又想從頭看?
沒問題! Home/End 切換讓你不管從上到下還是從下到上都滑得精彩!

接下來就來看看獨家功能的使用教學

文章編輯介面

Redo/Undo 可以記錄最多50筆編輯紀錄

只要按下下方工具列的 Redo/Undo 即可輕鬆回復編輯紀錄

讓你不再為行動版編輯而困擾



暫存文章回復功能可以在你放棄編輯時自動儲存文章內容

只要回到文章編輯頁面即可自動偵測是否有上次放棄編輯的文章內容

就算 App 曾經被關閉也可以回復

再也不怕辛苦打字的心血付諸東流



貼心防呆功能

按兩次返回鍵才會關閉文章編輯頁面

不須按其他確認離開按鈕

讓你輕鬆離開編輯介面又不怕按錯

就算真的按錯也有文章恢復功能

安心,放心又貼心

文章瀏覽

無邊框瀏覽模式,往下滑動自動隱藏占空間的工具列

往上滑動自動出現,讓你輕鬆瀏覽無負擔



ANSI/MOBI 模式自由切換(ANSI 模式在其他 App 稱為整頁模式)

看鄉民的藝術不再需要花兩倍流量

文章太長想直接 End 也沒問題

反向順向瀏覽隨你高興

看新推文只要按 End 重新整理即可看見鄉民最新回應

一鍵多工的按鍵設計讓你踏入鄉民的思考領域




推文分段預覽功能

懶得自己分段卻又不知道自動分段結果到底長怎樣?

按一下預覽分段讓你知道推文結果長怎樣



帳號任意門

讓你不管在看板頁,文章列表頁還是看文章推文

你愛怎麼換帳號就怎麼換帳號

其實全 PTT 只有我一個人,不信我換個帳號推一樣的話給你看

後記

開發這款 App 沒有想像中那麼容易

一來這是我的第一個 Android 專案

二來網路上的資源實在不夠多

光是 ANSI 控制碼還有資料傳輸規則就讓我崩潰了好幾次

原本只想花兩個月就完成,到現在做了快五個月

雖然中間有很多時間是在做介面的視覺設計,有時一調一整天就過去了...

於是為了不讓其他開發者遇到跟我一樣的困擾

在這幾天我會發布幾篇開發 PTT 行動版的教學文章

讓各位有興趣的開發者可以一起加入

讓 PTT 行動版有更好的瀏覽體驗

畢竟有競爭才會有進步

希望大家一起努力來提升台灣的軟體競爭力

Java - JSON 資料解析基本教學

2015-11-19 1:53 PM

JSON 算是目前主流的資料交換格式

跟 XML 比較起來 JSON的優點就是檔案小很多

但可讀性卻一點也不輸給 XML(這裡的可讀性是指"人")

以下就來簡單介紹 JSON 格式 以及如何解析資料

將 JSON 字串轉為物件 或是將物件轉換為 JSON 字串

在這裡會用到 Google 維護的 Gson Library

程式碼範例
// 陣列表達
[string1, string2, string3]

// 物件表達
{key1 : value1, key2 : value2, key3 : value3}

// 基本JSON表達式
{
  "id": 1,
  "name": "Hello JSON",
  "friends": [
    {
      "id": 2,
      "name": "Hello JAVA",
    },
    {
      "id": 3,
      "name": "Hello Andoird",
    }
  ],
  "certified": true
}

// 將 JSON 字串轉換為物件
return new GsonBuilder()
            // 若不須將日期自動轉換為 Date 物件則不須加入 setDateFormat 方法
            .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
            .create()
            // json 為 JSON字串, YourJsonObject.class 就是要轉換為的物件類型
            .fromJson(json, YourJsonObject.class);

// 若物件內的名稱與 JSON 內的名稱有些許差異時
// 可以用 @SerializedName("field_name") 指定
@SerializedName("issued_at")
private String fieldName;

// 若想轉換為 Map 格式 可使用以下方式 
Type t = new TypeToken<Map<String,Machine>>() {}.getType();
Map<String,Machine> map = (Map<String,Machine>) new Gson().fromJson("json", t);

// 若 JSON 內容最外層為陣列 而想轉換為 List 時可使用以下方式
Type t = new TypeToken<List<SearchResult>>() {}.getType();
List<SearchResult> list = (List<SearchResult>) new Gson().fromJson("json", t);

// 將物件轉為 JSON 格式字串
return new GsonBuilder()
            // 指定 Gson 不預先 escape html 的某些字元(如 "<", ">")
            .disableHtmlEscaping()
            .create()
            .toJson(this);
各項資料連結
Android - HttpURLConnection 基本教學 取得網頁資料(HTML, XML, JSON)
Android - 使用 HttpURLConnection 自動轉址失效的解決方式
google/gson - GitHub
JSON - MDN

Android - UncaughtException 很抱歉, app 已停止的錯誤處理

2015-11-18 6:00 PM

有時在實機測試會遇到 很抱歉,XXX已停止的錯誤並跳開應用程式

這種情況只靠 Log4J 是抓不到錯誤訊息的

因此 Debug 就非常困難 沒有錯誤訊息根本不知道哪裡有問題

那麼要如何取得相關的錯誤訊息呢?

由於是未處理的 Exception (稱作 UncaughtException)

因此會直接拋到 UI Thread 然後報錯 應用程式就停止了

要處理這類 UncaughtException 只要在 Application 加入以下程式碼即可

程式碼範例

public class App extends MultiDexApplication {

    // 建立 Logger 將以此輸出錯誤訊息
    private final Logger logger = Logger.getLogger(App.class);

    // 建立 UncaughtExceptionHandler UI Thread 的錯誤訊息會拋給這個物件處理
    private Thread.UncaughtExceptionHandler androidDefaultUEH;
    private Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() {
        public void uncaughtException(Thread thread, Throwable ex) {
            // print 錯誤訊息
            logger.error("Uncaught exception is:", ex);
            // 回報給預設錯誤處理
            androidDefaultUEH.uncaughtException(thread, ex);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        // 初始化 Log4J
        ConfigureLog4J.configure();

        // 建立 Handler 並指定為預設處理 Handler
        androidDefaultUEH = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(handler);
    }
}

各項資料連結
Android Handling the Unexpected
Android Developers - Thread.UncaughtExceptionHandler

Android - ObjectAnimator 的 Easing 進階動畫效果

2015-11-17 3:37 PM

使用 ObjectAnimator 實現動畫效果 這篇文章裡

我們學會了如何使用 Android 原生的 ObjectAnimator 實現動畫效果

在這裡我們要用第三方的 Framework 實現一些較進階的動畫效果

若有做過介面相關特效的人員應該很熟悉一個叫做 Easing 的函數

但預設的 ObjectAnimator 能用的函數很少 而且名稱也不同

於是我們就須使用以下兩個開源的 Libraries

JakeWharton/NineOldAndroidsdaimajia/AnimationEasingFunctions

達成可指定 Easing 函數 以及同時運行多個動畫參數的效果

以下將以原本隱藏在畫面右方的選單元件往左滑動顯示的特效作為範例

程式碼範例
// 首先請先在專案加入這兩個 dependency
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.daimajia.easing:library:1.0.1@aar'

// 在這裡使用的都是第三方的類別 名稱都跟原生 API 長得一模一樣 千萬不要搞錯
import com.daimajia.easing.Glider;
import com.daimajia.easing.Skill;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;

// 若之前狀態為 Gone 將無法取得寬度 因此在設為 Visible 後再以 AsyncTask 取得寬度設定動畫
mainMenuHolder.setVisibility(View.VISIBLE);

mainMenuHolder.post(new Runnable() {
    @Override
    public void run() {
     // 由於此範例是在 Fragment 內執行 因此需呼叫 getActivity 才能取得畫面尺寸
        Display display = getActivity().getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        int width = size.x;

        // 建立動畫物件 將選單由最右方向左滑出 直至顯示完整元件
        ObjectAnimator objectAnimator1 = ObjectAnimator.ofFloat(mainMenu, "x", width - mainMenu.getWidth());
        // 建立動畫物件 將選單外的畫面變暗的效果
        ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(mainMenuMask, "alpha", 1f);

        // 建立動畫監聽事件 在動畫開始時設定為 VISIBLE
        // 這是由於在隱藏選單時同樣會加入此監聽事件 並在動畫結束時隱藏的關係
        objectAnimator1.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart (Animator animation){
                mainMenu.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationEnd (Animator animation){
            }

            @Override
            public void onAnimationCancel (Animator animation){
            }

            @Override
            public void onAnimationRepeat (Animator animation){
            }
        });

        // 將剛剛建立的動畫加入動畫陣列
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                // 在此可設定 Easing 的函數 有開發過互動式介面的應該很熟悉
                // 若不清楚有那些可以設定 差別又在哪裡 可以看下方的 Easing Functions 參考連結
                Glider.glide( Skill.QuadEaseOut, 300, objectAnimator1 ),
                Glider.glide( Skill.QuadEaseOut, 300, objectAnimator2 )
        );

        // 設定動畫時間間隔 請以動畫陣列內執行時間最長的為主
        set.setDuration(300);
        // 開始執行動畫
        set.start();
    }
});
各項資料連結
Android - 使用 ObjectAnimator 實現動畫效果
Easing Functions
GitHub - JakeWharton/NineOldAndroids
GitHub - daimajia/AnimationEasingFunctions
Android - ObjectAnimator

Android - 使用 ObjectAnimator 實現動畫效果

12:51 PM

之前在開發 Flash 及 Javascript 上的介面時

我習慣用 GreenSock 開發的 TweenLite Library

因為他的執行效率非常好 而且使用也很方便直覺

但可惜的是他沒有支援 Android

而在另一篇介紹的 TweenEngine 雖然使用上很類似

詳見 Android - 使用 Tweener 加入互動特效

可是常常會出現動畫效果出不來或是直接報錯給你看

於是就萌生了使用原生 Android 動畫 API 的想法

以下就來介紹 ObjectAnimator 的使用方法

這裡將以淡出 View 為範例 並在完全淡出後設定 Visible 為 GONE

程式碼範例
// 建立動畫操作物件
ObjectAnimator objectAnimator = ObjectAnimator
                    // 指定動畫效果
                    .ofFloat(articleAuthorDataHolder, "alpha", 1.0f, 0.0f)
                    // 指定動畫時間長度
                    .setDuration(300);

// 加入動畫監聽 
objectAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {

    }

    // 動畫執行完成後將 View 設定為 GONE
    @Override
    public void onAnimationEnd(Animator animation) {
        articleAuthorDataHolder.setVisibility(View.GONE);
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }
});

// 開始動畫
objectAnimator.start();

各項資料連結
Android - 使用 Tweener 加入互動特效
Android - ObjectAnimator

Java - Thread 執行緒的 lock, wait, 與 notify

2015-11-09 6:17 PM

在使用多執行緒我們常常需要使用到 synchronize 確保值的一致性

但是有一些情況很容易造成資料錯亂

這時我們就必須將某一些 Thread lock 住

直到確認其他 Thread 的前置工作完成了 才執行其他 Thread 的後續工作

以下就來介紹執行緒的 lock 該如何設計

本範例將以同一個 class 內的多個執行緒做個簡單示範

程式碼範例
// 宣告一個 final 的變數 將作為參數傳遞至各執行緒
private final Object locker = new Object();

// 宣告另一個變數 作為判斷是否需要 lock 執行緒
// 至於在各執行緒間的傳遞方式有很多種 在這裡就不多作介紹
private boolean commandLock = false;

// Thread 1
while(true){
    if( !isConnected() ){
        lostConnection = true;
        break;
    }

    // 設定為 lock 狀態
    commandLock = true;

    // Do something

    // 設定為 unlock 狀態 並喚醒上一個等待中的Thread
    // 注意此處的 synchronized 是確保同時間只有一個執行緒操作 locker 物件
    commandLock = false;
    synchronized (locker) { locker.notify(); }
}

// Thread 2
public void reloadPage(){
    commandLocker();
    command( "Do reload" );
}

// Locker
private void commandLocker(){
    // 檢查 lock 狀態 若為 lock 狀態則進行 wait 動作
    // 直至 lock 狀態結束
    synchronized (locker) {
        while(commandLock){
            try {
                locker.wait();
            } catch (InterruptedException e) {
                logger.error("Thread sleep error in command locker section.");
            }
        }
    }
}

可能有人會看過在 while loop 中不使用 obj.wait(); 的作法

而是使用 Thread.sleep(millis); 的方式

但此方式容易消耗過多的效能進行不必要的 loop 執行

而且若 millis 沒有設定好 若同時有其他 Thread.sleep(); 的動作

很有因為時間差而可能造成 locker 永遠不會被解除的問題

但若單純使用空的 while 那效能的浪費是非常可怕的

因此還是使用 synchronized 加上 locker 物件的方式會較適當

各項資料連結
Java - Synchronized

Android - AlertDialog 的使用與建置

2015-11-08 11:06 AM

Android 的 AlertDialog 是一個非常好用的提示視窗元件

初始化非常的方便 且可應付大多數的情況

可以修改提示字元 確認按鈕字元 取消按鈕字元

設置 Listener 也相當的簡便

以下就來介紹該如何使用這個方便的控制元件

程式碼範例
// 建立元件
deleteAlert = new AlertDialog.Builder(this);
// 設置提示標題
deleteAlert.setTitle("貼心小提醒");
// 設置提示內容
deleteAlert.setMessage("確定要刪除嗎?");
// 設置確認按鈕顯示文字與 Listener
deleteAlert.setPositiveButton("確定", onClickPositiveDeleteArticle);
// 設置取消按鈕顯示文字與 Listener
deleteAlert.setNegativeButton("取消", onClickNegativeDeleteArticle);
// 設置使用者按下提示元件的其他地方與按下返回鍵導致取消的 Listener
deleteAlert.setOnCancelListener(onCancelDeleteArticle);

// 按下確認的 Listener
private DialogInterface.OnClickListener onClickPositiveDeleteArticle = new DialogInterface.OnClickListener(){
    @Override
    public void onClick(DialogInterface dialog, int which) {
        // TODO: Do something
    }
};

// 按下取消的 Listener
private DialogInterface.OnClickListener onClickNegativeDeleteArticle = new DialogInterface.OnClickListener(){
    @Override
    public void onClick(DialogInterface dialog, int which) {
        // TODO: Do something
    }
};

// 按下提示視窗之外的部分與按下返回鍵的 Listener
private DialogInterface.OnCancelListener onCancelDeleteArticle = new DialogInterface.OnCancelListener(){
    @Override
    public void onCancel(DialogInterface dialog) {
        // TODO: Do something
    }
};
各項資料連結
Android - AlertDialog

Android - 顯示 EditText 的虛擬鍵盤

2015-11-04 6:20 PM

若要在顯示某個 view 時

或是在按下其他按鈕時自動顯示指定 EditText 的虛擬鍵盤

可以使用以下方式

隱藏虛擬鍵盤使用方式詳見此篇介紹

Android - 隱藏 EditText 的虛擬鍵盤

程式碼範例
private EditText articleContentCommentEditor;

// 取得 focus
articleContentCommentEditor.requestFocus();
// 顯示虛擬鍵盤
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(articleContentCommentEditor, InputMethodManager.SHOW_IMPLICIT);
各項資料連結
Android - 隱藏 EditText 的虛擬鍵盤
Android Developer