owned this note
owned this note
Published
Linked with GitHub
# Android 前端學習筆記
歡迎補充
# Ch1 上傳照片
## 方法一:上傳正方形照片, 自動剪裁(Eka)
僅示範從相片總管中上傳照片, 拍照功能較複雜沒研究, 若有人研究再幫補充。
* 在layout先設好一個ImageView元件ivPhotograph, 用來放照片
```java=
private ImageView ivPhotograph = findViewById(R.id.iv_setting_photograph);
//在layout先設好一個ImageView元件, 用來放照片
```
* 監聽ImageView點擊事件, 當ImageView被點擊則開啟相片總管, 讓使用者在相片總管中選擇照片
```java=
public static final int OPEN_ALBUM_REQUEST = 1;//定義一個常數, 後面會用到
private void openAlbumAfterClickPhoto(){
ivPhotograph.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
//比較跳轉頁面的寫法:跳轉頁面的intent是明確的宣告要跳轉到哪個頁面, 這裡則是指定特定地Android內建功能, 讓使用者在相片總管中選擇一張照片
startActivityForResult(intent, OPEN_ALBUM_REQUEST);
// 選擇相片後, 需要回傳該相片的URI(類似檔案的存放路徑)到目前的Activity中, 之後才能把照片放到ivPhotograph上
// 所以不能用startActivity(intent), 而用startActivityForResult, 執行完選擇照片後, 自動回傳該資料的URI, 並返回目前的Activity
// startActivityForResult執行完後, Android會自動呼叫Activity中的onActivityResult()把相片的資料吐出來
// onActivityResult(), 類似onCreate, 請自己在Activity中Override
// 最一開始自行定義的OPEN_ALBUM_REQUEST常數,則是為了在執行onActivityResult()時, 判斷是何者何時發出的startActivityForResult
// (如果Activity中使用多次不同的startActivityForResult才有辦法辨認)
}
});
```
* 將選擇的照片解碼, 轉成Bitmap格式, 存入ivPhotograph
```java=
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//Android call onActivityResult時, 會把OPEN_ALBUM_REQUEST傳入requestCode
//並自動把取得的資料內容傳入data、執行結果傳入resultCode
if (resultCode == RESULT_OK) {
switch (requestCode){
case OPEN_ALBUM_REQUEST:
Uri uri = data.getData(); //把data中相片的uri取出
try {
ContentResolver cr = this.getContentResolver();
InputStream inputStream = cr.openInputStream(uri);
//開啟檔案路徑, 讀取檔案, 轉成inputStream(輸入串流)型態
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
//透過inputStream輸入串流, 將檔案轉成bitmap
ivPhotograph.setImageBitmap(getCircledBitmap(bitmap));
//將Bitmap裁切後, 設定到ImageView
} catch (FileNotFoundException e) {
Log.e("Exception", e.getMessage(), e);
}
}
}
}
public Bitmap getCircledBitmap(Bitmap bitmap) { //把照片裁切為圓形
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
return output;
}
```
* 目前照片只能存到記憶體中, 還不知道怎麼傳到後端或存到硬碟裡...歡迎幫忙補充
* 方法二除了正方形的照片外, 還能剪裁長方形照片, 也可設定取用相簿的權限, 方法二實現部分的程式碼前半部與方法一類似, 可參考其他部分
## 方法二:上傳正方形、長方形照片, 自動剪裁(境)
(可轉向長方形、正方形照片)
==先寫一個類別==
```java=
public class ImageOrientationUtil {
private static final String SCHEME_FILE = "file";
private static final String SCHEME_CONTENT = "content";
public static void closeSilently(@Nullable Closeable c) {
if (c == null) return;
try {
c.close();
} catch (Throwable t) {
// Do nothing
}
}
public static int getExifRotation(File imageFile) {
if (imageFile == null) return 0;
try {
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
// We only recognize a subset of orientation tag values
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default:
return ExifInterface.ORIENTATION_UNDEFINED;
}
} catch (IOException e) {
// Log.e("Error getting Exif data", e);
return 0;
}
}
@Nullable
public static File getFromMediaUri(Context context, ContentResolver resolver, Uri uri) {
if (uri == null) return null;
if (SCHEME_FILE.equals(uri.getScheme())) {
return new File(uri.getPath());
} else if (SCHEME_CONTENT.equals(uri.getScheme())) {
final String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
Cursor cursor = null;
try {
cursor = resolver.query(uri, filePathColumn, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
final int columnIndex = (uri.toString().startsWith("content://com.google.android.gallery3d")) ?
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) :
cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
// Picasa images on API 13+
if (columnIndex != -1) {
String filePath = cursor.getString(columnIndex);
if (!TextUtils.isEmpty(filePath)) {
return new File(filePath);
}
}
}
} catch (IllegalArgumentException e) {
// Google Drive images
return getFromMediaUriPfd(context, resolver, uri);
} catch (SecurityException ignored) {
// Nothing we can do
} finally {
if (cursor != null) cursor.close();
}
}
return null;
}
private static String getTempFilename(Context context) throws IOException {
File outputDir = context.getCacheDir();
File outputFile = File.createTempFile("image", "tmp", outputDir);
return outputFile.getAbsolutePath();
}
@Nullable
private static File getFromMediaUriPfd(Context context, ContentResolver resolver, Uri uri) {
if (uri == null) return null;
FileInputStream input = null;
FileOutputStream output = null;
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
FileDescriptor fd = pfd.getFileDescriptor();
input = new FileInputStream(fd);
String tempFilename = getTempFilename(context);
output = new FileOutputStream(tempFilename);
int read;
byte[] bytes = new byte[4096];
while ((read = input.read(bytes)) != -1) {
output.write(bytes, 0, read);
}
return new File(tempFilename);
} catch (IOException ignored) {
// Nothing we can do
} finally {
closeSilently(input);
closeSilently(output);
}
return null;
}
}
```
==AndroidManifest.xml中使用==
```java
<uses-permission android:name="android.permission.CAMERA"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
```
==implementation==
```java=
implementation 'com.squareup.picasso:picasso:2.71828'
```
==實現==
```java=
private static final int CHOOSE_PHOTO = 1;
//檢查許可權
public void checkPermission() {
ivCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(TogetherStepActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(TogetherStepActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
//發現沒有許可權,呼叫requestPermissions方法向用戶申請許可權,requestPermissions接收三個引數
//第一個是context,第二個是一個String陣列,我們把要申請的許可權名放在陣列中即可,第三個是請求碼,只要是唯一值就行
} else {
openAlbum();//有許可權就開啟相簿
}
}
});
}
public void openAlbum() {
//通過intent開啟相簿,使用startactivityForResult方法啟動actvity,會返回到onActivityResult方法,所以我們還得複寫onActivityResult方法
//Intent intent = new Intent("android.intent.action.GET_CONTENT");//這是打開檔案總管的寫法
//intent.setType("image/*");//類型
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);//打開相簿
startActivityForResult(intent, CHOOSE_PHOTO);
}
//彈出視窗向用戶申請許可權
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);//彈出授權的視窗,這條語句也可以刪除,沒有影響
//獲得了使用者的授權結果,儲存在grantResults中,判斷grantResult中的結果來決定接下來的操作
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openAlbum();
} else {
Toast.makeText(this, "授權失敗,無法操作", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK) {
handleImageOnkitKat(data);
}
break;
default:
break;
}
}
@TargetApi(19)
private void handleImageOnkitKat(Intent data) {
Uri uri = data.getData();
int rotateAngle = ImageOrientationUtil
.getExifRotation(ImageOrientationUtil
.getFromMediaUri(
this,
getContentResolver(),
uri));
Picasso.get()
.load(uri)
.rotate(rotateAngle)
.into(ivPlanPhoto);
}
```
# Ch2 推播(Eka)
兩個常見的推播出現的地方
![](https://i.imgur.com/Ff2DDbq.jpg)
![](https://i.imgur.com/QuBfhp7.png)
* 示範在onCreate中執行推播, 若要在程式關閉的情況下執行, 可能要研究一下怎麼用後端執行
```java=
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);
setNotification(); //自訂推播方式
View view = getWindow().getDecorView(); //得到當前的view
sendComposeMsg(view); //執行推播
}
```
* 自訂推播方式, 並開啟推播方式設定頁面
```java=
private void setNotification(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String channelId = "compose"; //給定目前設定的推播方式一個ID, 以便之後用到
String channelName = "推播"; //推播方式的名稱
int importance = NotificationManager.IMPORTANCE_DEFAULT; //推播的重要度
createNotificationChannel(channelId, channelName, importance); //創建新的推播方式
openChannelSetting("compose"); //開啟推播方式設定頁面
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createNotificationChannel(String channelId, String channelName, int importance) {
NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
NotificationManager notificationManager = (NotificationManager) getSystemService(
NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
}
public void openChannelSetting(String channelId)
{
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, channelId);
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null)
startActivity(intent);
}
```
推播方式設定頁面:
![](https://i.imgur.com/f3WwSxb.png)
* 執行推播
```java=
public void sendComposeMsg(View view) {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(this, "compose") // 傳入之前定義的推播方式ID, 宣告執行此推播時的推播方式
.setContentTitle("私信") //推播標題
.setContentText("有人私信向你提出问题") //推播內容
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_person_24dp) //推播的圖示
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_person_24dp))
.build();
manager.notify(101, notification);
}
```
P.S. 還有一些不同的推播設定參數(這裡沒有全部列出), 可以上網看看怎麼設定成你想要的方式
# Ch3 Spinner選單(查德)
基礎設定
private Spinner spn; //宣告
spn = findViewById(R.id.login_spn) //綁定layout畫面當中的Spinner元件
給訂下拉選單的文字有三種方式:
### 1.靜態方式
Value檔的String設定
```
<string-array name="divide_list">
<item>管理者</item>
<item>員工</item>
</string-array>
```
在XML當中就決定 spinner裡面的參數,把上面的string-array指定給Spinner元件
`android:entries="@array/divide_list"`
### 2. 動態綁定選單內容
```java
//以登入畫面為例,
public class LoginActivity extends AppCompatActivity {
private Spinner spn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
//onCreate階段開始
//綁定元件
spnIdentify = findViewById(R.id.login_spn_identify);
//決定選單要輸入的資料
final String[] spnList = new String[] {"員工", "管理者"};
ArrayAdapter arrayAdapter = new ArrayAdapter(
LoginActivity.this, //顯示的Context
R.layout.support_simple_spinner_dropdown_item, //下拉選單的item
spnList //選單匯入的資料
);
//將Adapter設給Spinner
spnIdentify.setAdapter(arrayAdapter);
spnIdentify.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
//點選項目後執行的動作
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
tvShow.setText(spnList[position]);
}
//未點選項目執行的動作(可不寫)
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
```
PS. 還有一個是連 View 都客製化的方法,有需求再請參閱
# Ch4 ScrollView滑動畫面(采云)
在xml樣式表內ScrollView的使用方式。
**注意:裡面只能包著一個layout,同時包兩個會失效。**
###### 垂直軸向滑動 - 程式碼:
```java=
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout ... 我是內容 >
</ScrollView>
```
###### 水平軸向滑動 - 程式碼:
```java=
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout ... 我是內容 >
</HorizontalScrollView>
```
###### 圖例:
![](https://i.imgur.com/7JAtn5w.png)
<br><br>
當心查到過期的無效設定:
###### ~~android:scrollX:以像素為單位設置水平方向滾動的的偏移值。~~
###### ~~android:scrollY:以像素為單位設置垂直方向滾動的的偏移值
# Ch5 ConstraintLayout (獄友)
## 為什麽要用ConstraintLayout?
在開發過程中經常能遇到一些覆雜的UI,可能會出現布局嵌套過多的問題,嵌套得越多,設備繪制視圖所需的時間和計算功耗也就越多。簡單舉個例子:
![](https://i.imgur.com/ViAaUTc.png)
假設現在要寫一個這樣的布局,先放一個垂直的LinearLayout,裡面放兩個水平的LinearLayout,然後在水平的LinearLayout裏面放TextView,然後再調整TextView個別的LayouWeight或margin、padding等屬性,有些人甚至會塞入空白的TextView,只為了使元件擺在特定位置。然而這樣的寫法不僅嵌套了兩層LinearLayout,間距位置寫死,畫面的元件數量也可能被迫增加。
但若使用ConstraintLayout的話,由於每個物件都會綁定著特地物件,以此達到對應的位置效果。如使用得當,相對原本的做法更靈活,也較省資源。還有一點就是由於ConstraintLayout可以按照比例約束控件位置和尺寸,如此一來能夠更好地支援螢幕大小不同的機型。
## 初步理解ConstraintLayout
* ConstraintLayout的核心概念就是Constraint(約束),藉由建立元件和元件之間的彼此約束,來產生對應的相對位置。
![](https://i.imgur.com/L3IV01R.png)
> 解讀 : ImageView左邊綁定了左邊界,右邊綁定了右邊界,並各有24dp的邊距縮減
* 再舉例來說,假設畫面中有此二元件 TextView1 和 TextView2,如下圖
![](https://i.imgur.com/Cx5GzW2.png)
則TextView1 在 TextView2 的左邊,反之TextView2 在 TextView1 的右邊
則佈局則如下
<TextView
android:id="@+id/TextView1"
...
android:text="TextView1" />
<TextView
android:id="@+id/TextView2"
...
app:layout_constraintLeft_toRightOf="@+id/TextView1" />
* 又如果要呈現下圖
![](https://i.imgur.com/C3Mzquc.png)
解讀 : TextView3 在 TextView1的下方
<TextView
android:id="@+id/TextView1"
...
android:text="TextView1" />
<TextView
android:id="@+id/TextView2"
...
app:layout_constraintLeft_toRightOf="@+id/TextView1" />
<TextView
android:id="@+id/TextView3" //新增TextView3
...
app:layout_constraintTop_toBottomOf="@+id/TextView1" />
除了列舉的layout_constraintTop_toBottomOf="@+id/元件"(綁定top於指定元件的Bottom)這個關鍵定位語法以外,下面列出相對定位的常用屬性:
layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf
而元件本身對應的位置屬性如下圖
![](https://i.imgur.com/rDnmL4T.png)
## 調整邊距、定位角度、偏移量
* **調整邊距**
在使用ConstraintLayout的布局還是會遇到要微調邊距的情況(如微調linearLayout的內元件的margin屬性一般)。
如下圖即為 " TextView距離上邊界跟下邊界各10dp "
![](https://i.imgur.com/bTTQGd8.png)
<TextView
android:id="@+id/TextView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
app:layout_constraintLeft_toLeftOf="parent" //這行不可少
app:layout_constraintTop_toTopOf="parent"/> //這行不可少
注意:在設定與任何元件或邊界產生邊距時,都必須有先綁定的動作(此範例為綁定邊界)。
ConstraintLayout的邊距常用屬性如下:
android:layout_marginStart
android:layout_marginEnd
android:layout_marginLeft
android:layout_marginTop
android:layout_marginRight
android:layout_marginBottom
---
* 角度定位
角度定位指的是可以用一個角度和一個距離來約束兩個空間的中心。舉個例子:
<TextView
android:id="@+id/TextView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/TextView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintCircle="@+id/TextView1"
app:layout_constraintCircleAngle="120"
app:layout_constraintCircleRadius="150dp" />
上面例子中的TextView2用到了3個屬性:
app:layout_constraintCircle="@+id/TextView1"
app:layout_constraintCircleAngle="120"(角度)
app:layout_constraintCircleRadius="150dp"(距離)
指的是TextView2的中心在TextView1的中心的120度,距離為150dp,效果如下:
![](https://i.imgur.com/Ag9C6nM.png)
---
* 偏移量
ConstraintLayout 中預設有個特性,就是若某元件約束左右個綁定其他兩個元件,在沒有調整偏移量的情況下該元件會自動擺到左右元件的水平中央。
![](https://i.imgur.com/F9yINIE.png)
上圖中間的BUTTON是會自動擺到水平置中的。換句話說,如果元件上下左右都綁定邊界,在沒有調整偏移量的情況下就會在跑到布局的正中央。如下圖
![](https://i.imgur.com/Ljn1g4O.png)
複習一下,以下圖為例,下面TextView1綁定了左右邊界,由於未設定偏移量,所以自動水平居中後,再使用layout_marginLeft="100dp"向右偏移了100dp。
![](https://i.imgur.com/LYbELxh.png)
* 比例位置偏移
那如果要實現如LinearLayout中layoutWeight的比例偏移效果,ConstraintLayout還提供了另外一種偏移的屬性:
layout_constraintHorizontal_bias 水平偏移
layout_constraintVertical_bias 垂直偏移
舉個例子:
<TextView
android:id="@+id/TextView1"
...
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
效果如下:
![](https://i.imgur.com/DKvGRsR.png)
假如現在要實現水平偏移,給TextView1的layout_constraintHorizontal_bias賦一個範圍為 0-1 的值,假如賦值為0,則TextView1在布局的最左側,假如賦值為1,則TextView1在布局的最右側,假如假如賦值為0.5,則水平居中,假如假如賦值為0.3,則更傾向於左側。
垂直偏移同理。
## 其他效果
上面是整理觀念以及核心用法,如果需要了解更細的功能如輔助工具等,我覺得以下連結都講解得非常生動易懂,可以參照
https://www.jianshu.com/p/17ec9bd6ca8a
https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/672036/
## 補充
如果Activity的Layout設為ConstraintLayout,當手機旋轉方向時,整個畫面結構很有可能亂掉 => 可以強制不讓畫面旋轉,請在manifests中對應的Activity加上下面的code。
<activity
android:name=".RegisterActivity"
android:screenOrientation="portrait">
</activity>
# Ch6 基本Menu選單+側開式選單(獄友)
## Menu
先上圖,這裡的基本Menu指的是下圖所示,在理解側開式選單前要先搞懂這個。
![](https://i.imgur.com/ZsWG4lv.png)
先說明一下Android App選單的限制:
1. 選單最多只能有兩層
2. 選單只會顯示文字,不會顯示圖形
3. 選單是Activity的一部分,因此建立選單的程式碼必須寫再Activity,因此建立選單的程式碼必須寫在Activity的程式檔裡頭。
4. 兩種方式建立menu,a.程式碼直接導入方法建立, b.在res/menu裡頭建立選單的定義檔,再用程式載入使用。
5. 上述兩種方法都會用到onCreateOptionMenu()和onOptionItemSelected()這兩個方法。
開始吧。
* ### onCreateOptionMenu() 和 onOptionItemSelected()的功能。
> onCreateOptionMenu() = 創建menu
它們和onCreate()一樣,都是提供給Android系統呼叫用。下面會介紹使用範例。
首先,先建立需傳入方法的兩個選項的id,這裡我用常數定義。
//這裡純粹是給ID值,不一定要照著寫
private static final int MENU_ABOUT = Menu.FIRST;
private static final int MENU_EXIT = Menu.FIRST+1;
再來直接在 Activity中 Override 方法 onCreateOptionMenu(),並且會用到Menu類別的add()方法,這個方法就是在選單中加入一個選項。
> add(群組id,選項id,選項的排列次序,選項名稱)
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0,MENU_ABOUT,1,"關於這個APP)
.setIcon(android.R.drawable.ic_dialog_info);
menu.add(0,MENU_EXIT,2,"結束")
.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
return true;
}
此時menu的建構就完成了。
> onOptionItemSelected() = 建立點擊(選取)事件
而當使用者點選了選單的某一個選項時,Android系統會呼叫 onOptionItemSelected()這個方法,系統會偵測你點到MENU中的哪一個ITEM。我是用switch case當作範例,可參考。
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
switch (menuItem.getItemId()){
case MENU_ABOUT:
Toast.makeText(MainActivity.this,"選擇關於APP",Toast.LENGTH_LONG).show();
return true; //處理完必須回傳true
case MENU_EXIT:
Toast.makeText(MainActivity.this,"選擇離開",Toast.LENGTH_LONG).show();
return true; //處理完必須回傳true
}
return false;
}
如果要在選單裡面建立選單,變成兩層,就必須呼叫Menu參數的addSubMenu()方法。使用方式跟add()一模模一樣樣,只不過它會先傳回一個SubMenu物件,然後在用該物件呼叫add()。以下範例可以參考。
private static final int MENU_ABOUT = Menu.FIRST;
private static final int MENU_EXIT = Menu.FIRST+1;
private static final int MENU_MUSIC = Menu.FIRST+2;
private static final int MENU_PLAY_MUSIC = Menu.FIRST+3;
private static final int MENU_STOP_PLAYING_MUSIC = Menu.FIRST+4;
二層式選單創建時寫法一樣,但有兩層的選項要建立。
public boolean inCreateOptionsMenu(Menu menu){
//先建立最外層的選項SubMenu
SubMenu subMenu = menu.addSubMenu(0,MENU_MUSIC,0,"背景音樂");
//用add()來建立SubMenu內的選項
subMenu.add(0,MENU_PLAY_MUSIC,0,"播放背景音樂");
subMenu.add(0,MENU_STOP_PLAYING_MUSIC,1,"停止播放背景音樂");
return true;
}
* ### 建立XML格式的選單資源檔。
前面介紹的選單式在程式內建立,如果程式的選單式固定的,那麼可以換成用選單資源檔。以下為建立步驟。
> 首先,在res中建立一個名為menu的directory(資料夾)。
>接下來創建一個xml檔,內容如下:
>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:icon="@android:drawable/ic_media_ff"
android:title="選項一">
<menu>
<item
android:id="@+id/menuItemPlayBackgroundMusic"
android:title="內選項A" />
<item
android:id="@+id/menuItemStopBackgroundMusic"
android:title="內選項B" />
</menu>
</item>
<item
android:id="@+id/menuItemAbout"
android:icon="@android:drawable/ic_dialog_info"
android:title="選項二" />
<item
android:id="@+id/menuItemExit"
android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:title="選項三" />
</menu>
* 選單資源檔最外層是<menu>標籤,裏頭的每一個<item>標籤表示一個選項。
* <item>標籤中的id屬性是指定項目的id,程式碼會用這個id來判斷使用者點選的項目。
* title屬性是項目名稱,icon屬性是小圖示。這些屬性的設定方式跟介面元介屬性用法一樣。
* app:showAsAction這裡有幾個參數可以設定
never:不會在上方Action顯示選項
ifRoom:如果上方Action區域有空間,就會顯示
always:一定會顯示在Action區域上,但不建議使用
withText:除了icon圖示外,也顯示item的標題文字
* 儘管有設定icon,選當項目必須是Action Item 的時候,小圖示才會出現,純item不會有icon。
* 在選單有兩層的狀態下,第一層的<item>標籤不需要設定id,他只是一個入口,不是真正的選項。
* 這個layout布局檔可以在主程式中用MenuInflator吹出來,事件的處。
public boolean onCreateOptionsMenu(Menu menu){
MenuInflater inflater = getMenuInflater();
//吹出menu,參考res/menu/menu.xml
inflater.inflate(R.menu.menu, menu);
return true;
}
> 事件的部分一樣由onOptionItemSelected()來處理,跟上面一樣。就不再重複貼CODE囉。
## 側開式選單 DrawerLayout
所謂Navigation Drawer並不是一個單一元件,而是由 DrawerLayout 元件包覆著 NaviationView元件來實現的,所以我會叫他DrawerLayout。
* 首先要先理解,NavigationDrawer可以包含兩個部份,分別是HEADER跟MENU。
![](https://i.imgur.com/JDvwv0t.png)
> DrawerLayout必需用布局檔案來建立。 布局檔架構比較複雜,以下是範例 :
###
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
//側開選單布局檔最外層必須為DrawerLayout(包覆整個布局)
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:openDrawer="start">
//用<include>去嵌入一個xml檔,這裡放的是你的主畫面
<include layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
//Header跟Menu放NavigationView裡面,可分別設定HEADER和MENU的介面布局資源檔
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start" //這裡必須設定為start才會顯示正確位置
android:fitsSystemWindows="true"
app:headerLayout="@layout/drawer_header"
app:menu="@menu/menu" />
</androidx.drawerlayout.widget.DrawerLayout>
> * 上述布局檔中NavigationView的menu屬性下的menu要自己先建立,因為其實NavigationView包覆著的就是menu,要有menu才有選項可選。(如menu本身有兩層,內外會預設展開)。
> * app:headerLayout的布局檔也要自己建立,不需要header也可以不用。
---
* 下一步,在Activity 中使用NaviationDrawer。
上個步驟我們設定好了元件,DrawerLayout包覆著NavigationView,裡面有著menu,此時我們會用到NavigationView裡面的OnNavigationItemSelectedListener來處理點選menu的動作。
* 先取得DrawerLayout以及NavigationView兩個元
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navView = indViewById(R.id.nav_view);
* 設定Listener處理按下選單項目時的操作 :
navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
switch (menuItem.getItemId()){
case R.id.menuItemAbout:
Toast.makeText(MainActivity.this,"選項一",Toast.LENGTH_LONG).show();
drawerLayout.closeDrawers(); //點擊後關閉側滑選單
break;
}
return false;
}
});
此時從畫面側滑就會出現Navigation Drawer了。
## 漢堡
三條橫線的按鈕會利用ActionBarDrawerToggle來實現,達到ActionBarDrawerToggle+Toolbar+Navigation Drawer這三個交互效果,當然你要用Button也可以,但就會少了酷炫的預設效果。
結構不複雜,自訂Toolbar取代原本的ActionBar,並從原本的Navigation Drawer架構新增幾個元件就可以。
Toolbar取代原本的ActionBar之前(就是顯示頁面名稱那條橫Banner),要先關掉ActionBar,去res/values/styles.xml 調整:
//Theme改成NoActionBar
<resource>
<style name="AppTheme" parnet="Theme.AppCompat.Light.NoActionBar">
...(其他程式碼)
</resource>
主程式布局檔中嵌入ToolBar元件:
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
</com.google.android.material.appbar.AppBarLayout>
漢堡來了,主程式新增~~漢堡~~元件變數:
ActionBarDrawerToggle drawerToggle;
主程式中取得ToolBar元件,把它設為ActionBar:
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
建立ActionBarDrawerToggle並讓他和DrawerLayout元件偕同運作:
drawerToggle = new ActionBarDrawerToggle(this,drawerLayout,toolbar,R.string.app_name,R.string.app_name);
//傳入ToolBar元件
drawerLayout.addDrawerListener(drawerToggle);
drawerLayout.setFitsSystemWindows(true);
drawerToggle.syncState();
Done,全世界都驚呆了。
# Ch7 TimePicker滑動(嘉芸)
![](https://i.imgur.com/AkmHPeC.png)
## Java
```java=
public class Main2Activity extends AppCompatActivity {
private TextView mDisplayDate;
private DatePickerDialog.OnDateSetListener mDateSetListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mDisplayDate = findViewById(R.id.test);
mDisplayDate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
//重點:選擇主題- Theme_Holo_Light_Dialog_MinWidth
DatePickerDialog dialog = new DatePickerDialog(
Main2Activity.this,
android.R.style.Theme_Holo_Light_Dialog_MinWidth,mDateSetListener,
year,month,dayOfMonth);
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialog.show();
}
});
mDateSetListener = new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
month = month+1;
String date= month +"/" + dayOfMonth + "/" +year;
mDisplayDate.setText(date);
}
};
}
```
* xml檔內容
```
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".Main2Activity">
<TextView
android:id="@+id/test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="100dp"
android:textSize="30sp"
android:text="TextView" />
</LinearLayout>
```
## Kotlin
```kotlin=
tvBirthday = view.findViewById(R.id.personal_info_tv_birthday)
tvBirthday.setOnClickListener {
var calendar= Calendar.getInstance()
var year = calendar.get(Calendar.YEAR)
var month = calendar.get(Calendar.MONTH)
var dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
//重點:選擇主題- Theme_Holo_Light_Dialog_MinWidth
var dialog = DatePickerDialog(this@RegisterActivity,
android.R.style.Theme_Holo_Light_Dialog_MinWidth,mDateSetListener,
year,month,dayOfMonth)
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
}
mDateSetListener = DatePickerDialog.OnDateSetListener { view, year, month, dayOfMonth ->
val month1 = month+1
var date= "$month1 / $dayOfMonth / $year"
tvBirthday.setText(date)
}
```
# Ch8 底部導航(嘉芸)
## Kotlin
* 在res創建一個menu資料夾,開一個home.xml
![](https://i.imgur.com/CyCt0X7.png)
```
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:title="@string/menu_navigation_home"
//放入自己想要的icon進入drawable資料夾。以下的圖檔名稱為home。
android:icon="@drawable/home" />
<item
android:id="@+id/navigation_card"
android:title="@string/menu_navigation_card"
android:icon="@drawable/card"
/>
<item
android:id="@+id/navigation_discussion"
android:icon="@drawable/discussion"
android:title="@string/menu_navigation_discussion" />
<item
android:id="@+id/navigation_setting"
android:icon="@drawable/setting"
android:title="@string/menu_navigation_setting" />
</menu>
```
* Main Activity
```kotlin=
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottomNavigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
}
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when(item.itemId){
R.id.navigation_home -> {
switchFragement(HomeFragment())
return@OnNavigationItemSelectedListener true}
R.id.navigation_card -> {
switchFragement(CardFragment())
return@OnNavigationItemSelectedListener true}
R.id.navigation_discussion -> {
switchFragement(DiscussionFragment())
return@OnNavigationItemSelectedListener true}
R.id.navigation_setting -> {
switchFragement(SettingFragment())
return@OnNavigationItemSelectedListener true}
}
false
}
private fun switchFragement(fragment: Fragment){
val fragmentTransction = supportFragmentManager.beginTransaction()
fragmentTransction.replace(R.id.fragment,fragment)
fragmentTransction.commit()
}
}
```
* activity_main.xlm 加入fragment及buttonNavigation tools
![](https://i.imgur.com/D7jUCDH.png)
```
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/fragment"
android:name="com.example.babyflash.HomeFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
app:itemIconTint="@android:color/white"
app:itemTextColor="@android:color/white"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/home">
</com.google.android.material.bottomnavigation.BottomNavigationView>
</androidx.constraintlayout.widget.ConstraintLayout>
```
* Fragment的內容,以其中一個為例,其他一致。
```kotlin=
package com.example.babyflash
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
/**
* A simple [Fragment] subclass.
*/
class HomeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home, container, false)
}
}
```
* fragment_home.xml
```
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:id="@+id/fragment_home"
tools:context=".HomeFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="@color/colorPrimaryDark"
android:textSize="32sp"
android:text="home" />
</FrameLayout>
```
## Java
1. 創建一個empty activity
# ch9 Toolbar(Action Bar) (境)
## 內建Toolbar
![](https://i.imgur.com/wgr2yiE.png)
#### 步驟1:style 設定
```java=
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
```
#### 步驟2:創建 Toolbar
<補充> 在Layout裡設定, 貼在constraint/linear layout中
```java=
<androidx.appcompat.widget.Toolbar
android:id="@+id/profile_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/main_color"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:logo="@drawable/ic_arrow_back"
app:title="SavingPal"
app:titleMarginStart="40dp"></androidx.appcompat.widget.Toolbar>
```
<補充>讓App名稱置中
![](https://i.imgur.com/swM3V1h.png)
---
↓使用內建Logo的方式
![](https://i.imgur.com/NfgxDSu.jpg)
![](https://i.imgur.com/rHE3kVO.png)
#### 步驟3:
<補充> 定義private Toolbar toolbar的時候, 不要選到Toolbar(android.widget), 否則不能用, Toolbar(androidx.appcompat.widget)才是對的.
```java=
toolbar = findViewById(R.id.profile_toolbar);
setSupportActionBar(toolbar);
@Override //如果需要menu的話
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu,menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
switch (id){
case R.id.about:
Toast.makeText(this, "click about", Toast.LENGTH_SHORT).show();
case R.id.share:
Toast.makeText(this, "click share", Toast.LENGTH_SHORT).show();
}
return true;
}
```
#### 補充:設定左上角 NavigationIcon
```java=
private void init(){
toolbar = findViewById(R.id.profile_toolbar);
setSupportActionBar(toolbar); //一定要在setNavigationIcon上面, 否則點擊NavigationIcon無效
setNavigationIcon();
}
private void setNavigationIcon(){
toolbar.setNavigationIcon(R.drawable.ic_settings_40dp); //設定NavigationIcon圖案
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("clicked");
}
});
}
```
#### 補充:設定右上角 Menu 的資料
![](https://i.imgur.com/uOuenEW.png)
* 於 res/menu 創建一個 menu_example.xml
* showAsAction 屬性:
* always:項目一定放在ActionBar (有資料說不建議使用,沒查為什麼)
* never:項目不放在ActionBar,放在最右側的選項選單
* ifRoom:如果ActionBar有空間,該項目放在ActionBar
```java=
<item
android:title="Share"
android:id="@+id/ahare"></item>
<item
android:title="About"
android:id="@+id/about"></item>
<item
android:title="Exit"
android:id="@+id/exit"></item>
<item
android:title="Person"
android:id="@+id/person"
android:icon="@drawable/ic_person"
app:showAsAction="always"></item>
<item
android:title="Alarm"
android:id="@+id/alarm"
android:icon="@drawable/ic_alarm"
app:showAsAction="always"></item>
```
==如果要使用內建Toolbar並且讓文字置中的話==
(這個我沒實現過,我是用方法二自定義的方式)
↓方法一
![](https://i.imgur.com/3XXlat7.png)
![](https://i.imgur.com/pOC1nPL.png)
## 自定義Toolbar
![](https://i.imgur.com/e7AIsvG.png)
```java=
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/profile_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/main_color"
app:popupTheme="@style/AppTheme">
//放TextView之類的
</androidx.appcompat.widget.Toolbar>
```
Main
```java=
private Toolbar toolbar;
toolbar = findViewById(R.id.profile_toolbar);
setSupportActionBar(toolbar);
```
# Ch10 Tab頁籤、ViewPager、Fragment(獄友)
簡單說明 : Tab頁籤、ViewPager、Fragment的幾乎是100%會應在APP上,下面會用比較好理解的方式解說。
首先,先確定您有
> implementation 'com.google.android.material:material:1.0.0'
## tab頁籤
> tab頁籤就好像網頁瀏覽器上的頁籤一樣,可以理解成一排按鈕的組合(?
![](https://i.imgur.com/NoWhHoO.png)
## ViewPager
> 既然tab可以理解成按鈕,也就代表得搭配個點擊的交互內容,而最常看到的就是结合ViewPager使用。哪什麼又是ViewPager呢?
* ViewPager 是一個內建就有的元件,支援左右滑動。
* 它是一個View的容器,可以在其中添加其他的view 。
* 它控制的邏輯很像RecycleView,也有自己的adapter去管理內容。
* 我們需要它來的特性來讓"tab頁籤跟內容的切換"也支援左右滑動。
## Fragment(片段)
>有了ViewPager這個容器,那我們要丟什麼內容進去呢,這裡就要介紹必學的Fragment(片段),它的說明如下:
* Fragment的設計主要目的是為了在大型螢幕上支援更多動態和彈性 UI 設計。舉例來說,手機上點擊元件跟事件觸發,可能需要兩個畫面來交互處理,手機可能一次只能呈現一個畫面。但在平板上如有Fragment的包裝,點擊的按鈕跟點擊事件要呈現的畫面便可能放在同一個畫面,如下圖所示。Fragment的活用讓APP在不同螢幕空間皆可提供最佳的使用者體驗。
![](https://i.imgur.com/sHoujak.png)
* 由於它有著類似Activity的特性,您可以將片段想成是 Activity 的模組化區段,片段擁有自己的生命週期、接收自己的輸入事件,而且您可以在 Activity 執行時新增或移除片段 (有點像是您可以在不同 Activity 中重複使用的「子 Activity」)。
* App的操作畫面由一個或多個Fragment組成的話,設計上可以有更多變化。
* 片段必須一律嵌入 Activity 中,片段在APP執行期間是可以動態移除跟加入的,而Activity 的生命週期會直接影響片段的生命週期。 例如,當 Activity 暫停時,其中的所有片段也會一併暫停;而當 Activity 遭到刪除時,所有片段也會一併刪除。
## 綜合實現
接下來會用到以下物件:
1. ViewPager
2. FragmentPagerAdapter
3. TabLayout
4. Fragment
> 範例 :
* 我將會用TabLayout來切換兩個內容不同的畫面,TabLayout先擺好,把想丟的內容內容丟進兩個Fragment,再把兩個Fragment丟進ViewPager裡面,再設定TabLayout和ViewPager交互切換。
注意這只是示意圖,tab所切換的是下面整個ViewPager包含的部分
![](https://i.imgur.com/fglGFZT.png)
* 我們放一個TabLayout在頂端,然後中間下面擺一個ViewPager,並給他們該有的id,布局檔範例如下。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.1"/>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="0.9">
</androidx.viewpager.widget.ViewPager>
</LinearLayout>
* 再來,我假設我這兩個畫面的Fragment呈現的內容截然不同,所以我創了兩個Fragment類別。
public class FirstFragment extends Fragment {
public FirstFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.first_fragment,container,false);
return v;
}
}
* 第二個Fragment類(當然你要用同一種Fragment建立兩個實體也可以)
public class SecondFragment extends Fragment {
public SecondFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.second_fragment,container,false);
return v;
}
}
* 建立這兩個Fragment所需要的布局檔,這裡丟個TextView到正中央而已,就不另外寫了。
* 下一步,建立ViewPager所需要的Adapter
public class CustomPagerAdapter extends FragmentPagerAdapter {
public CustomPagerAdapter (FragmentManager fragmentManager){
super(fragmentManager);
}
private final int FRAGMENT_SIZE = 2; //我給它總數兩個
@Override
public Fragment getItem(int position) {
Fragment fragment = null;
switch(position){
case 0 : fragment = new FirstFragment();
break;
case 1 : fragment = new SecondFragment();
break;
case 2 : fragment = new ThirdFragment();
break;
}
return fragment;
}
@Override
public int getCount() {
return FRAGMENT_SIZE;
//這裡是頁面的總數,關係到等會兒tab會創造幾個,所以一定要理解。
}
}
* 再來是主頁面
public class MainActivity extends AppCompatActivity {
private CustomPagerAdapter customPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//建立PagerAdapter
customPagerAdapter = new CustomPagerAdapter(getSupportFragmentManager());
//把PagerAdapter設定給ViewPager
ViewPager viewPager = findViewById(R.id.viewPager);
viewPager.setAdapter(customPagerAdapter);
//把ViewPager設定給TabLayout
//setupWithViewPager(viewPager)這個方法會讓tabLayout去偵測FragmentAdapter
裡面的getCount()並依據來增加tab頁籤
TabLayout tabLayout = findViewById(R.id.tabLayout);
tabLayout.setupWithViewPager(viewPager);
//頁籤創造後再自行調整內容
tabLayout.getTabAt(1).setText("頁籤一");
tabLayout.getTabAt(2).setText("頁籤二");
}
}
Done !
* 關於FragmentAdapter有一些細節建議也了解一下。
https://kknews.cc/zh-tw/news/l83pngg.html
* 更多細節
https://www.jianshu.com/p/fde38f367019
# Ch11 跳轉頁面動畫(Eka)
不囉嗦,直接上圖
![](https://i.imgur.com/ilcK1F5.gif)
## 頁面跳轉
跳轉頁面分為兩種:
1. Activity A -> Activity B (step1)
2. Activity B 回到 Activity A (step2)
step1:
* 若跳轉後還要回到原本的ActivityA, 在Activity A跳轉需要用startActivityForResult,並傳入自定義的requestCode(GO_TO_SETTING_ACTIVITY)。
* 用這種方式回原本的Activity A, Activity A則不會再呼叫onCreate, 則是call onActivityResult。
``` java=
public static final int GO_TO_SETTING_ACTIVITY = 2;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Intent intent = new Intent(this, SettingActivity.class);
startActivityForResult(intent, Global.GO_TO_SETTING_ACTIVITY);
}
```
step2:
* 從Activity B回Activity A,並傳入自定義的responseCode(RESULT_CANCELED<-代表回Activity後不特別在onActivityResult做事)。
``` java
private void setNavigationIcon(){
Intent intent = new Intent(SettingActivity.this, MainActivity.class);
setResult(RESULT_CANCELED, intent);
finish();
}
```
## 動畫
step1:
* 要將跳轉Activity的動畫改為自訂動畫, 需要先在res目錄下新增資料夾, 並在裡面加入自訂的動畫xml檔
![](https://i.imgur.com/kI40Sod.png)
* no_anim.xml
```
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromXDelta="0"
android:toXDelta="0">
</translate>
```
* slide_from_right.xml
```
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromXDelta="100.0%p"
android:toXDelta="0.0" >
</translate>
```
* slide_to_right.xml
```
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromXDelta="0.0"
android:toXDelta="100.0%p">
</translate>
```
step2:
* 接著設定Activity A -> Activity B的動畫, 在Activity A加上overridePendingTransition, 指定跳轉動畫。
* 跳轉時想讓Activity A保持不動, Activity B滑入
* 給定參數(R.anim.slide_from_right)代表Activity B出現時由右側進入, 參數(R.anim.no_anim)代表Activity A消失時保持不動
* 注意在xml中設定兩個動畫持續時間(duration)需要保持一致,否則可能會像前面動圖出現黑屏的部份
```java=
public static final int GO_TO_SETTING_ACTIVITY = 2;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Intent intent = new Intent(this, SettingActivity.class);
startActivityForResult(intent, Global.GO_TO_SETTING_ACTIVITY);
//加入下面這行
overridePendingTransition(R.anim.slide_from_right, R.anim.no_anim);
}
```
* 接著要在Activity B返回Activity A時加入動畫(Activity A不動, Activity B向右滑出), 則要在Activity B中, Override finish, 加上overridePendingTransition, 指定跳轉動畫。
```java=
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.no_anim, R.anim.slide_to_right);
}
```
完成啦~~~
# Ch12 搜尋Bar(RecycleView使用Filter)
範例:
在RecyclerView中使用搜尋Bar,
找到你輸入的關鍵字物件。
![](https://i.imgur.com/btgnTGc.png)
實作方法簡介:
1. 在RecyclerView實現篩選器,並且指定要篩選的屬性。
2. 在Activity實作RecyclerView,符合輸入文字會立即出現在畫面上。
## 1. RecycleView實現Filterable篩選介面
``` java
public class CustomRecycleViewAdapter
extends RecyclerView.Adapter<RecyclerView.ViewHolder>
implements Filterable { // 實現 Filterable(過濾)
private List<Order> orderArrayList;
private List<Order> orderArrayListFull;
public CustomRecycleViewAdapter(List<Order> orderArrayList) {
this.orderArrayList = orderArrayList;
//創建一個複製版的orderArraylist,用來修改
this.orderArrayListFull = new ArrayList<>(orderArrayList);
}
private class ItemViewHolder extends RecyclerView.ViewHolder {
//ItemViewHolder實作
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder
(@NonNull ViewGroup parent, int viewType) {
//onCreateViewHolder實作
}
@Override
public void onCreateViewHolder
(@NonNull RecyclerView.ViewHolder holder, int position) {
//onCreateViewHolder實作
}
@Override
public int getItemViewType(int position) {
//getItemViewType實作
}
@Override
public int getItemCount() {
//getItemCount實作
}
// ----------------------------------
@Override
public Filter getFilter() {
return myFilter;
}
//創造一個filter,實作內部function
private Filter myFilter = new Filter() {
//製作過濾器,回傳結果
@Override
protected FilterResults performFiltering(CharSequence constraint) {
List<Order> filteredList = new ArrayList<>();
//如果輸入值為空,就會顯示所有資料
if (constraint == null || constraint.length() == 0) {
filteredList.addAll(orderArrayListFull);
//如果有打字(過濾模式)
} else {
//constraint: 輸入內容
String filterPattern = constraint.toString().trim();
//如果order的關鍵字包含輸入文字,就將此order加入過濾列表
for (Order order : orderArrayListFull) {
if (order.getDeparturePlace().contains(filterPattern)) {
filteredList.add(order);
}
}
}
FilterResults results = new FilterResults();
results.values = filteredList;
return results;
}
@Override
protected void publishResults
(CharSequence constraint, FilterResults results) {
//清除原本資料
orderArrayList.clear();
//放入被篩選過的資料
orderArrayList.addAll((List) results.values);
notifyDataSetChanged();
}
};
}
```
## 2. 製作畫面
[](https://i.imgur.com/JbpwORS.png)
- 繪製一個search icon
drawable資料夾右鍵 > New > Vector Asset
點擊Clip Art圖示,在新視窗找search關鍵字,
換成放大鏡icon,改名字,完成。
![](https://i.imgur.com/Hwe3i4r.png)
---
[](https://i.imgur.com/oIDmjND.png)
- 產生menu資料夾
res > New > Android Resource Directory
類型選擇menu
![](https://i.imgur.com/lxVKyL9.png)
[](https://i.imgur.com/C5hIUki.png)
- 在menu資料夾右鍵產生xml檔案,名稱自取
New > Menu resource file
點進此xml檔,內容修改如下
``` xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--icon選剛剛製作的search icon-->
<!--showAsAction = ifRoom 將三個點變成search功能-->
<item android:id="@+id/order_search"
android:icon="@drawable/ic_search"
android:title="Search"
app:showAsAction="ifRoom"
app:actionViewClass="android.widget.SearchView"/>
</menu>
```
#### 有無 ifRoom 差異:![](https://i.imgur.com/0lVxMDs.png)
## 3. 在Activity使用SearchView
```java=
//實作menu
public boolean onCreateOptionsMenu(Menu menu) {
//使用inflater,以及剛創建的menu.xml
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.order_menu, menu);
//設定icon
MenuItem menuSearchItem = menu.findItem(R.id.order_search);
//設定searchView 和 searchView內文監聽
SearchView searchView = (SearchView) menuSearchItem.getActionView();
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
//recycleViewAdapter的filter去抓輸入文字
adapter.getFilter().filter(newText);
return false;
}
});
return true;
}
```
完成 ~
# Ch13 簡單月曆實作
說明:用Recycle View的Grid Layout呈現月曆功能實作(單月)。
## Activity
```java
public class MainStaffAcitvity extends AppCompatActivity {
private TextView tvTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_staff_acitvity);
tvTitle = findViewById(R.id.main_staff_activity_tv_title);
CalendarArray calendarArray = new CalendarArray(2020, 0, 1);//2020年1月1日
tvTitle.setText(calendarArray.getInitDay().getYear()+"年"+calendarArray.getInitDay().getMonth()+"月");
ArrayList<CalendarArray.Everyday> calendarList = calendarArray.getCalendarArry();
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(this, 7); //制定一個layout的格式縱向橫向方格
RecyclerView recyclerView = findViewById(R.id.staff_main_rv_calendar);//綁定recyclerView的樣式(在layout)
recyclerView.setLayoutManager(layoutManager);
CalendarAdapter calendarAdapter = new CalendarAdapter(calendarList); //將arrayList當中的資料經由adapter轉成recycleView的內容資訊
recyclerView.setAdapter(calendarAdapter);
}
public static class CalendarAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
private class ItemViewHolder extends RecyclerView.ViewHolder{
TextView tvDate;
TextView tvDayShift;
TextView tvNightShift;
public ItemViewHolder(@NonNull View itemView) {
super(itemView);
tvDate = itemView.findViewById(R.id.calerdar_tv_date);
tvDayShift = itemView.findViewById(R.id.calerdar_tv_day_shift);
tvNightShift = itemView.findViewById(R.id.calerdar_tv_night_shift);
}
}
private class AddItemViewHolder extends RecyclerView.ViewHolder{ //建立在表單最後的另外一個view
Button btnSave; //尾端的按鈕
public AddItemViewHolder(@NonNull View itemView) {
super(itemView);
btnSave = itemView.findViewById(R.id.calendar_item_btn_save);
}
}
private ArrayList<CalendarArray.Everyday> data;
public CalendarAdapter(ArrayList<CalendarArray.Everyday> arrayList){ //客製化Apapter的建構子放入ArrayList
data = new ArrayList<CalendarArray.Everyday>(arrayList);//把資料拷貝一分到新的arrayList裡面(拷貝後兩者不同步) 若為data = arrayList則為直接傳實體 若array放的是參考資料型態則拷貝會是地址
//外面arrayList與裡面data資料是否同步會衍伸出後面bug
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if(viewType == 1){
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.calendar_item_save, parent, false);
return new AddItemViewHolder(view);
}
View view = LayoutInflater.from(parent.getContext()) //什麼是viewGroup為何可以取得所在activity的context
.inflate(R.layout.calendar_item_grid_day, parent, false);
return new ItemViewHolder(view); //回傳一個包裝的這個view的view的viewholder
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
// if(holder instanceof ItemViewHolder){ //由類別判斷
if(getItemViewType(position) ==0){ //由position判斷
//viewHolder向下轉型成itemViewHolder
ItemViewHolder ivh = (ItemViewHolder)holder;
ivh.tvDate.setText(data.get(position).getDay()+"");
ivh.tvDayShift.setText("早班");
ivh.tvNightShift.setText("打烊班");
}else if(getItemViewType(position) == 1){
}
}
@Override
public int getItemViewType(int position){
if(position == data.size()){
return 1;
}
return 0;
}
@Override
public int getItemCount() {
return data.size() +1 ;
}
}
}
```
## 製作Recycle View(Grid layout)的資料
```java
public class CalendarArray {
private ArrayList<Everyday> calendarArry;
private Everyday initDay;
public static class Everyday{
private int year;
private int month;
private int day;
public Everyday(Calendar calendar){
this.year = calendar.get(Calendar.YEAR);
this.month = calendar.get(Calendar.MONTH);
this.day = calendar.get(Calendar.DATE);
}
public int getYear(){
return year;
}
public int getMonth(){
return month + 1;
}
public int getDay(){
return day;
}
}
public CalendarArray(int yyyy, int mm, int dd){
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, yyyy);
calendar.set(Calendar.MONTH, mm); //month start from 0
calendar.set(Calendar.DATE, dd);// day start from 0
initDay = new Everyday(calendar);
calendarArry = new ArrayList<>();
int weekday = calendar.get(Calendar.DAY_OF_WEEK);
Log.d("WEEK", weekday+"");
// define the vary first day of array
calendar.add(Calendar.DATE, (weekday -1)*(-1));
calendarArry.add(new Everyday(calendar));
for(int i = 0;i < 34; i++){
calendar.add(Calendar.DATE, 1);
calendarArry.add(new Everyday(calendar));
}
}
public ArrayList<Everyday> getCalendarArry(){
return calendarArry;
}
public Everyday getInitDay(){
return initDay;
}
}
```
# Ch14 recycler 回彈到最上方 (牧)
recycler 回彈到最上方
* 有滑動效果
kotlin code
```kotlin=
fun scrollToPosition(){
recyclerView.smoothScrollToPosition(0 )
recyclerView.layoutManager
}
```
* 直接跳轉(沒有滑動效果)
```kotlin=
fun scrollToPosition(){
recyclerView.scrollToPosition(0 )
recyclerView.layoutManager
}
```
<補充> 畫面置底
Java
```java=
recyclerView.scrollToPosition(customAdapter.getItemCount()-1);
//移到最後一個項目
```
# Ch15 recycler view holder 嵌套 viewpager (牧)
recycler view holder 嵌套 viewpager
class TopFiveArticleItemViewHolder(itemView : View) : BaseViewHolder<Top5Articles>(itemView){
private val viewPager2 : ViewPager2 = itemView.findViewById(R.id.view_pager_for_top_five)
private var timer : Timer = Timer()
private lateinit var top5Articles : Top5Articles
private val tabLayout : TabLayout = itemView.findViewById(R.id.tab_dot)
init{
viewPager2.orientation = ViewPager2.ORIENTATION_HORIZONTAL
}
class Factory(clickFuncBuilder: ClickFuncBuilder?) : BaseViewHolder.Factory(clickFuncBuilder){
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BaseViewHolder<out BaseModel> {
val view : View = LayoutInflater.from(parent.context).inflate(R.layout.item_view_top_5 , parent , false)
view.tag = "Top5"
Log.d("clickAbout" , super.clickFunctionMap[R.id.constraint_layout_top5].toString())
return TopFiveArticleItemViewHolder(view )
}
override fun getType(): String {
return Top5Articles.TYPE
}
}
override fun bind(baseModel: BaseModel) {
top5Articles = baseModel as Top5Articles
//這裡從外部拿到前五大熱門文章list
viewPager2.adapter = TopFiveAdapter(top5Articles.getTop5() )
TabLayoutMediator(tabLayout , viewPager2 ,
TabLayoutMediator.TabConfigurationStrategy { tab, position ->
}).attach()
startAutoScrolling()
}
//設定自動輪播
private fun startAutoScrolling(){
stopAutoScrolling()
val task = object : TimerTask() {
override fun run() {
Handler(Looper.getMainLooper()).post {
val position : Int = (viewPager2.currentItem + 1) % 5
viewPager2.setCurrentItem(position,true)
}
}
}
timer = Timer()
timer.scheduleAtFixedRate(task , 8000 ,5000)
}
private fun stopAutoScrolling() {
timer.cancel()
timer.purge()
}
fun canScrollHor(dir : Int) : Boolean{
return viewPager2.canScrollHorizontally(dir)
}
}
class TopFiveAdapter(arrayList : ArrayList<ArticleInfo>) : RecyclerView.Adapter<TopFiveAdapter.Top5ViewHolder>() {
private val articleInfoArrayList = arrayList
private lateinit var currentArticleInfo: ArticleInfo
inner class Top5ViewHolder(itemView: View ) : RecyclerView.ViewHolder(itemView) {
private val textViewTitle : TextView = itemView.findViewById(R.id.top_five_article__text_view_title)
private val imageViewImage : ImageView = itemView.findViewById(R.id.top_five_article_image_view)
private lateinit var articleInfo: ArticleInfo
init {
textViewTitle.isSingleLine= true
//這裡是可以被偵測點擊的
itemView.setOnClickListener {
//把id傳進bundle 跳轉下一頁,並在內文頁load文章內容
val intent = Intent(itemView.context, ContentActivity::class.java)
val bundle = Bundle()
bundle.putLong(Global.CONTENT_BUNDLE_ID , articleInfo.id)
intent.putExtras(bundle)
itemView.context.startActivity(intent)
}
}
fun bind(articleInfo : ArticleInfo){
this.articleInfo = articleInfo
if(articleInfo.getBitMap() == null){
var bitmap : Bitmap? = null
NetworkController.instance.getBitmap(articleInfo.url , object : BitmapCallBack{
override fun onFailure(call : Call , errorMessage: String) {
}
override fun onResponse(call : Call ,response: Response) {
bitmap = NetworkController.instance.decodeBitmap(response)
}
override fun onComplete() {
Handler(Looper.getMainLooper()).post {
imageViewImage.setImageBitmap(bitmap)
}
}
})
}
textViewTitle.text = articleInfo.title
imageViewImage.setImageBitmap(articleInfo.getBitMap())
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Top5ViewHolder {
//
val view : View = LayoutInflater.from(parent.context).inflate(R.layout.item_view_top_five_article,parent,false)
return Top5ViewHolder(view )
}
override fun getItemCount(): Int {
return articleInfoArrayList.size
}
override fun onBindViewHolder(holder: Top5ViewHolder, position: Int) {
currentArticleInfo = articleInfoArrayList[position]
holder.bind(currentArticleInfo)
}
}
# Ch16 長按刪除資料(嘉芸)
```java=
LinearLayout linearLayout;
linearLayout = itemView.findViewById(R.id.baby_info_layout);
linearLayout.setOnLongClickListener(new View.OnLongClickListener() {@Override public boolean onLongClick(View v) {
AlertDialog dialog = new AlertDialog.Builder(MyInfoActivityRV.this)
.setTitle("確定刪除嗎?")
.setNegativeButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dataBaby.remove(getLayoutPosition()-1);
notifyDataSetChanged();
}
})
.show();
Log.d(TAG,"dataBaby.size():"+dataBaby.size()+"");
return true;
}
});
```