Search

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