分類
Uncategorized

PhotoView 使用介紹

PhotoView 是一個開源函式庫,主要提供圖片縮放處理,預設已提供兩指縮放,雙擊放大等等基本的手勢功能。
因此若需求相當簡單,可以考慮使用該函式庫來快速完成功能。
GitHub : https://github.com/chrisbanes/PhotoView
相關的相依性以及初始化可以參考官方github。

以下紀錄基本的使用方式:

1. Dependency

1-1.在 project module 的 build.gradle 加入

allprojects {
	repositories {
        maven { url "https://jitpack.io" }
    }
}

1-2.在 module module 的 build.gradle 加入

dependencies {
    implementation 'com.github.chrisbanes:PhotoView:latest.release.here'
}

 

2. declare in xml

<com.github.chrisbanes.photoview.PhotoView
    android:id="@+id/photo_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

注意 com.github.chrisbanes.photoview.PhotoView 命名必須完全相同
 

3. Using in code

3-1. init by resource id

PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageResource(R.drawable.image);

3-2. init by bitmap

String photoPath = PHOTO_FILE_PATH + "/" + photoIndex + ".jpg";
Bitmap bitmap = BitmapFactory.decodeFile(photoPath);
PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageBitmap(bitmap);

 
4. 雙擊圖片或拖曳圖片即有縮放效果,完全不用加入任何控制的程式碼。

分類
Uncategorized

RecyclerView 基本使用架構

這個基本使用架構主要套用MVP Patterrn,架構中包含1個顯示在V(Activity)的RecyclerView,提供了以下幾個基本功能:
1.RecyclerView可以即時檢視,新增,修改,刪除項目(ItemView)
2.可設定每頁應該包含項目數,切換頁面。
3.透過FinalDB來儲存項目。
4.自定義RecyclerView的ItemView
首先從架構的PackageDiagram來介紹,該架構包含3個package。
主要的package為左上角的basic package,共有6個class,包含MVP組成元件以及RecyclerView的元件。
右上角的database package,共有2個class為提供儲存資料的功能。
下方的common package,只有1個class,提供取得Application context。
Package 相依性如下圖

 
接著從主要的basic package 的 class diagram開始介紹。
MVP構成的元件為RecyclerItemManager(M),RecyclerViewPresenter(P),
RecyclerViewActivity(V),可以看出V和M不直接溝通,而是透過P交互
(雖然在標準的MVP中,V和P是透過interface交互,為了方便介紹架構就不另外為了V和P建立interface)。
RecyclerViewActivity為介面層,只負責初始化UI以及接受使用者輸入並轉發給RecyclerViewPresenter,不包含任何業務邏輯。
RecyclerViewPresenter為邏輯層,主要接受介面層轉發的指令,並再轉發給Model,當Model處理完回傳資料給邏輯層,處理介面邏輯後再回傳給介面層。
RecyclerItemManager為Model,會持有RecyclerItem list,該list是資料的中心,主要處理業務邏輯,不含任何的介面邏輯。
而 RecyclerViewAdapter 和 RecyclerViewHolder 為構成RecyclerView的元件。
RecyclerViewHolder負責管理ItemView(顯示在RecyclerView的項目)。
RecyclerViewAdapter負責建立RecyclerViewHolder以及綁定數據到holder。
最後是RecyclerItem為資料類別,主要是提供數據並顯示在RecyclerView的ItemView。

接著來看看source code。
首先是RecyclerViewActivity,先來看看該Activity使用的介面檔案。

<LinearLayout 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:orientation="vertical"
  tools:context=".basic.RecycleViewActivity">
  <LinearLayout
    android:layout_width="match_parent"
    android:orientation="horizontal"
    android:layout_height="wrap_content">
    <Button
      android:id="@+id/recycler_view_btn_add"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="add"/>
  </LinearLayout>
  <android.support.v7.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
</LinearLayout>

相當簡單,只包含1個Button,1個RecyclerView。
接著看看RecyclerViewActivity。

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.codefoxx.recycleviewexample.R;
import java.util.List;
public class RecycleViewActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = RecycleViewActivity.class.getSimpleName();
  private RecyclerViewPresenter mRecyclerViewPresenter;
  private RecyclerView mRecyclerView;
  private RecyclerViewAdapter mRecyclerViewAdapter;
  private Button mAdd;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.recycler_view_main_activity);
    mRecyclerViewPresenter = new RecyclerViewPresenter(this);
    initData();
    initUI();
  }
  private void initData() {
    mRecyclerViewPresenter.loadData();
  }
  private void initUI() {
    mAdd = (Button) findViewById(R.id.recycler_view_btn_add);
    mAdd.setOnClickListener(this);
    initRecyclerView();
  }
  private void initRecyclerView() {
    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
    linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    mRecyclerView.setLayoutManager(linearLayoutManager);
    setScrollListenerToRecyclerView();
    mRecyclerViewAdapter = new RecyclerViewAdapter(
        mRecyclerViewPresenter.getItemsByPage(0));
    mRecyclerView.setAdapter(mRecyclerViewAdapter);
  }
  public void recyclerViewClickEvent(View view) {
    int position = mRecyclerView.getChildAdapterPosition(view);
    mRecyclerViewPresenter.showActionDialog(position);
  }
  private void setScrollListenerToRecyclerView() {
    mRecyclerView.addOnScrollListener(new OnScrollListener() {
      @Override
      public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        recyclerViewScrollToTopAndLoadPreviousPage(newState);
        recyclerViewScrollToBottomAndLoadNextPage(newState);
      }
    });
  }
  private void recyclerViewScrollToBottomAndLoadNextPage(int newState) {
    boolean isScrollToBottomEdge = !mRecyclerView.canScrollVertically(1);
    if (newState == RecyclerView.SCROLL_STATE_IDLE && isScrollToBottomEdge) {
      mRecyclerViewPresenter.loadNextPage();
    }
  }
  private void recyclerViewScrollToTopAndLoadPreviousPage(int newState) {
    boolean isScrollToTopEdge = !mRecyclerView.canScrollVertically(-1);
    if (newState == RecyclerView.SCROLL_STATE_IDLE && isScrollToTopEdge) {
      mRecyclerViewPresenter.loadPreviousPage();
    }
  }
  public void reloadRecyclerView(int page) {
    List<RecyclerItem> itemsOnPage = mRecyclerViewPresenter.getItemsByPage(page);
    mRecyclerViewAdapter = new RecyclerViewAdapter(itemsOnPage);
    mRecyclerView.setAdapter(mRecyclerViewAdapter);
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.recycler_view_btn_add:
        mRecyclerViewPresenter.addNewItem();
        break;
    }
  }
}

第16行為MVP的Presenter。
第18~19行為構成RecyclerView的基本元件,要使用RecyclerView必須一併使用這2個元件。
第21行為提供加入RecyclerView的項目用。
第27行為Presenter的初始化,把Activity傳入建構式中綁定View。
第28行初始化資料。
第29行初始化UI介面。
第33行為了保持View的簡單,委託Presenter讀取資料。
第37~38行初始化mAdd按鈕。
第39行初始化RecyclerView(重點)。
第43行初始化mRecyclerView變數。
第44~46行建立LinearLayoutManager為垂直並設定給mRecyclerView。
第47行為mRecyclerView設定滾動的監聽器,此監聽器中會偵測mRecyclerView是否已滾動到該頁最上方或最下方,再觸發讀取上一頁或下一頁的動作。
第48~49行初始化mRecyclerViewAdapter。
第50行設定Adapter到RecyclerView。
note:第42行的initRecyclerView為初始化RecyclerView的動作為固定用法。
第53~56行為設定點擊RecyclerView的事件,注意該方法名稱必須對應於RecyclerView的ItemView所使用的layout xml中的onclick名稱。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
  android:onClick="recyclerViewClickEvent"
...

第58~67行為設定監聽RecyclerView滾動的事件,在滾動中會判斷是否已到該頁最上方或最下方並觸發讀取上下頁的動作。
第69~74行即為當RecyclerView滾動到最下方並讀取下一頁的動作。
第76~81行即為當RecyclerView滾動到最上方並讀取上一頁的動作。
第83~87行為更新RecyclerView的動作,該動作會在model改變之後被觸發。
第90~97行為Activity的介面點擊事件。
接著是RecyclerViewAdapter

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.codefoxx.recycleviewexample.R;
import java.util.List;
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecycleViewHolder> {
  private static final String TAG = RecyclerViewAdapter.class.getSimpleName();
  private List<RecyclerItem> mRecycleItems;
  public RecyclerViewAdapter(List<RecyclerItem> source) {
    mRecycleItems = source;
  }
  @Override
  public RecycleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView = LayoutInflater
        .from(parent.getContext()).inflate(R.layout.recycle_item_view, parent, false);
    RecycleViewHolder viewHolder = new RecycleViewHolder(itemView);
    return viewHolder;
  }
  @Override
  public void onBindViewHolder(RecycleViewHolder holder, int position) {
    String listNumber = mRecycleItems.get(position).getListNumber();
    holder.mListNumber.setText(mRecycleItems.get(position).getListNumber());
    holder.mTime.setText(mRecycleItems.get(position).getTime());
    holder.mDescription.setText(mRecycleItems.get(position).getDescription());
    if (mRecycleItems.get(position).getIsLaunch()) {
      holder.mIsLaunch.setText("已啟用");
    } else {
      holder.mIsLaunch.setText("未啟用");
    }
    if (mRecycleItems.get(position).getIsVibrator()) {
      holder.mIsVibrator.setText("有震動");
    } else {
      holder.mIsLaunch.setText("無震動");
    }
  }
  @Override
  public int getItemCount() {
    return mRecycleItems.size();
  }
}

RecyclerViewAdapter為構成RecyclerView的元件,主要功能為建立ViewHolder以及綁定數據到ViewHolder。
基本的使用方式主要是覆寫3個方法(onCreateVieHolder, onBindViewHolder, getItemCount),並加入相關的實作內容。
第8行注意繼承RecyclerView.Adapter<RecycleViewHolder>
extends RecyclerView.Adapter為固定用法,<RecycleViewHolder>則是自定義類別,ViewHolder也是構成RecyclerView的基本元件。
第11行為adapter持有的數據,也是透過該數據綁定到ViewHolder上。
第18~23行為建立ViewHolder,在該方法中透過LayoutInflater產生需要的itemView並再將itemView傳入viewHolder中,最後回傳viewHolder。
注意第20行的R.layout.recycle_item_view為ItemView使用的介面檔。
note:該方法主要用意在建立viewHolder的介面,目前還未綁定數據到ViewHolder,綁定數據的動作是在onBindViewHolder方法。
第26~41行即為綁定數據到ViewHolder中。
第44~46行為回傳數據的大小。
接著來看看ItemView所使用的介面檔,該介面檔即是描述顯示在RecyclerView中的單個項目。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/basic_recycler_item_baisc"
  android:layout_width="match_parent"
  android:layout_height="110dp"
  android:descendantFocusability="blocksDescendants"
  android:orientation="vertical"
  android:onClick="recyclerViewClickEvent"
  android:weightSum="1">
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.6">
    <ImageView
      android:id="@+id/basic_recycler_item_iv_uplayout_upSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentTop="true">
    </ImageView>
    <TextView
      android:id="@+id/basic_recycler_item_tv_uplayout_list_number"
      android:textStyle="bold"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="20dp"
      android:layout_marginLeft="10dp"
      android:layout_alignParentLeft="true"
      android:layout_gravity="center"
      android:text="1"
      android:textSize="20sp"
      android:visibility="invisible"/>
    <TextView
      android:id="@+id/basic_recycler_item_tv_uplayout_time"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginLeft="10dp"
      android:layout_centerVertical="true"
      android:text="06:00"
      android:textSize="18sp"/>
    <TextView
      android:id="@+id/basic_recycler_item_et_uplayout_is_launch"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerInParent="true"
      android:text="已啟動"
      android:textSize="18sp"/>
    <ImageView
      android:id="@+id/basic_recycler_item_et_uplayout_is_launch_icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:layout_gravity="center_horizontal"
      android:layout_toLeftOf="@+id/basic_recycler_item_et_uplayout_is_launch"
      android:adjustViewBounds="true"
      android:background="#00000000"
      android:maxHeight="50dp"
      android:maxWidth="50dp"
      android:scaleType="fitCenter"/>
    <TextView
      android:id="@+id/basic_recycler_item_et_uplayout_is_vibrator"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginRight="10dp"
      android:layout_alignParentRight="true"
      android:layout_centerInParent="true"
      android:text="有震動"
      android:textSize="18sp"/>
    <ImageView
      android:id="@+id/basic_recycler_item_et_uplayout_is_vibrator_icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:layout_gravity="center_horizontal"
      android:layout_toLeftOf="@+id/basic_recycler_item_et_uplayout_is_vibrator"
      android:adjustViewBounds="true"
      android:background="#00000000"
      android:maxHeight="50dp"
      android:maxWidth="50dp"
      android:scaleType="fitCenter"
      />
  </RelativeLayout>
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.4">
    <ImageView
      android:id="@+id/basic_recycler_item_iv_downlayout_upSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentTop="true">
    </ImageView>
    <TextView
      android:id="@+id/basic_recycler_item_iv_downlayout_description_hint"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:text="描述:"/>
    <TextView
      android:id="@+id/basic_recycler_item_iv_downlayout_description"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_toRightOf="@+id/basic_recycler_item_iv_downlayout_description_hint"
      android:text="這是描述。"/>
    <ImageView
      android:id="@+id/basic_recycler_item_iv_downlayout_downSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentBottom="true"></ImageView>
  </RelativeLayout>
</LinearLayout>

這裡唯一需要注意的是第8行

android:onClick="recyclerViewClickEvent"

這裡為了方便介紹直接使用onClick屬性指定回調方法名稱,該方法名稱即為在
RecyclerViewActivity第53行定義的方法,簡單的說就是當點擊R.id.basic_recycler_item_baisc UI元件時會觸發RecyclerViewActivity第53行定義的方法。
接著是RecyclerViewHolder

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.codefoxx.recycleviewexample.R;
public class RecycleViewHolder extends RecyclerView.ViewHolder {
  private static final String TAG = RecyclerViewAdapter.class.getSimpleName();
  public LinearLayout mBasicLayout;
  public TextView mListNumber;
  public TextView mTime;
  public TextView mIsLaunch;
  public TextView mIsVibrator;
  public TextView mDescription;
  public RecycleViewHolder(View v) {
    super(v);
    mBasicLayout = (LinearLayout) v.findViewById(R.id.basic_recycler_item_baisc);
    mListNumber = (TextView) v.findViewById(R.id.basic_recycler_item_tv_uplayout_list_number);
    mTime = (TextView) v.findViewById(R.id.basic_recycler_item_tv_uplayout_time);
    mIsLaunch = (TextView) v.findViewById(R.id.basic_recycler_item_et_uplayout_is_launch);
    mIsVibrator = (TextView) v.findViewById(R.id.basic_recycler_item_et_uplayout_is_vibrator);
    mDescription = (TextView) v.findViewById(R.id.basic_recycler_item_iv_downlayout_description);
  }
}

ViewHolder相當簡單,主要是管理ItemView,若想保持封裝性也可以將public 欄位改為private並提供相對應的public方法。
以上就是RecyclerView所使用到有關View的元件。
接著介紹MVP的Presenter

import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import com.codefoxx.recycleviewexample.R;
import com.codefoxx.recycleviewexample.database.RecyclerItemDBWrapper;
import com.krtc.common.tool.DialogFactory;
import java.util.List;
import java.util.UUID;
public class RecyclerViewPresenter {
  private static final String TAG = RecyclerViewPresenter.class.getSimpleName();
  private RecycleViewActivity mRecycleViewActivity;
  public RecyclerViewPresenter(
      RecycleViewActivity recycleViewActivity) {
    mRecycleViewActivity = recycleViewActivity;
  }
  public void showActionDialog(final int position) {
    AlertDialog.Builder actionDialog = new Builder(mRecycleViewActivity);
    RecyclerItem item = RecyclerItemManager.getInstance()
        .getItem(position);
    actionDialog.setTitle("選擇動作" + "提醒為:" + item.getDescription());
    ArrayAdapter<CharSequence> actionDialogAdapter = ArrayAdapter.createFromResource(
        mRecycleViewActivity,
        R.array.basic_recycler_view_item_action_select, android.R.layout.select_dialog_item);
    actionDialog.setAdapter(actionDialogAdapter, new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        final int REVIEW = 0;
        final int ADD = 1;
        final int DELETE = 2;
        final int MODIFY = 3;
        if (which == REVIEW) {
          reviewItem(position);
        } else if (which == ADD) {
          addNewItem();
        } else if (which == DELETE) {
          deleteItem(position);
        } else if (which == MODIFY) {
          modifyItem(position);
        }
        dialog.dismiss();
      }
    });
    actionDialog.show();
  }
  private void modifyItem(int position) {
    RecyclerItemManager.getInstance().modifyItem(position);
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  private void deleteItem(int position) {
    RecyclerItemManager.getInstance().deleteItem(position);
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  public void addNewItem() {
    RecyclerItemManager.getInstance().addItem(generateNewItem());
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  private RecyclerItem generateNewItem() {
    RecyclerItem itemForAdd = new RecyclerItem();
    itemForAdd.setId(UUID.randomUUID().toString());
    itemForAdd.setTime("00:00");
    itemForAdd.setDescription("new description");
    itemForAdd.setIsLaunch(false);
    itemForAdd.setIsVibrator(false);
    itemForAdd.setListNumber("");
    return itemForAdd;
  }
  private void reviewItem(int position) {
    RecyclerItem recyclerItem = RecyclerItemManager.getInstance()
        .getItem(position);
    Toast.makeText(mRecycleViewActivity, recyclerItem.toString(), Toast.LENGTH_LONG)
        .show();
  }
  public void loadPreviousPage() {
    if (RecyclerItemManager.getInstance().isMorePreviousPage()) {
      showSwitchPreviousPageConfirmDialog();
    } else {
      showNoMorePreviousPageToast();
    }
  }
  private void showSwitchPreviousPageConfirmDialog() {
    DialogFactory
        .createTwoButtonAlertDialog(mRecycleViewActivity, "切換頁面", "是否切換到上一頁?", "否",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
              }
            }, "是", new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                int page = RecyclerItemManager.getInstance().decreaseCurrentPageIndex();
                mRecycleViewActivity.reloadRecyclerView(page);
              }
            }).show();
  }
  private void showNoMorePreviousPageToast() {
    Toast.makeText(mRecycleViewActivity, "已經沒有上一頁了", Toast.LENGTH_SHORT).show();
  }
  public void loadNextPage() {
    if (RecyclerItemManager.getInstance().isMoreNextPage()) {
      showSwitchNextPageConfirmDialog();
    } else {
      showNoMoreNextPageToast();
    }
  }
  private void showSwitchNextPageConfirmDialog() {
    DialogFactory
        .createTwoButtonAlertDialog(mRecycleViewActivity, "切換頁面", "是否切換到下一頁?", "否",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
              }
            }, "是", new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                int page = RecyclerItemManager.getInstance().increaseCurrentPageIndex();
                mRecycleViewActivity.reloadRecyclerView(page);
              }
            }).show();
  }
  private void showNoMoreNextPageToast() {
    Toast.makeText(mRecycleViewActivity, "已經沒有下一頁了", Toast.LENGTH_SHORT).show();
  }
  public List<RecyclerItem> getItemsByPage(int page) {
    return RecyclerItemManager.getInstance()
        .getItemsByPage(page);
  }
  public void loadData() {
    List<RecyclerItem> itemsForLoad = RecyclerItemDBWrapper.loadBasicRecyclerItem();
    RecyclerItemManager.getInstance().cleanAndAddItems(itemsForLoad);
  }
}

RecyclerViewPresenter主要接受RecyclerViewActivity轉發的指令,處理需要的邏輯之後再將該指令轉發給Model層,因此會持有對View和對Model的引用。
第15行持有對View的引用(mRecycleViewActivity)。
第17~20行藉由建構式傳入View,和mRecycleViewActivity連結。
第22~52行顯示動作對話框,當使用者點擊RecyclerView上的itemview時,該對話框顯示可操作動作。目前提供檢視,新增,刪除,修改,4個功能。
第54~57行為修改itemview動作。
第59~62行為刪除itemview動作。
第64~67行為新增itemview動作。
note:從以上動作內容可以看出一致性為首先委託RecyclerViewManager進行對model的修改,再通知View更新頁面。
第69~78行為新增itemview使用,這裡為了方便介紹所以新增類似的itemview,實際應用上可以提供使用者輸入新增的相關內容。
第80~85行為檢視itemview動作。
第87~93行為當RecyclerView滾動到該頁的最上方時讀取上一頁的內容。
第95~110行為顯示對話框提醒使用者是否切換到上一頁,使用者選擇否,不進行任何動作,使用者選擇是,就進行讀取上一頁的動作。
第112~114行為提醒使用者該頁已為第一頁。
第116~121行為當RecyclerView滾動到該頁的最下方時讀取下一頁的內容(相對於第87~93行的loadPreviousPage)。
第124~139行為顯示對話框提醒使用者是否切換到下一頁,使用者選擇否,不進行任何動作,使用者選擇是,就進行讀取下一頁的動作。
第141~143行為提醒使用者該頁已為最後頁。
第145~148行為委託RecyclerItemManager取得指定頁數的內容(ItemView)。
第150~153行為啟動RecyclerViewActivity時讀取儲存在資料庫的資料,注意這裡也是為了方便介紹所以直接在UI Thread操作資料庫,標準作法應該要在background thread進行。
接著介紹MVP的Model,RecyclerItemManager
RecyclerItemManager會持有一個RecyclerItem的List,該list的來源為資料庫的內容(RecyclerItemDBWrapper),並提供給RecyclerViewActivity來顯示。

import android.util.Log;
import .database.RecyclerItemDBWrapper;
import java.util.ArrayList;
import java.util.List;
public class RecyclerItemManager {
  private static final String TAG = RecyclerItemManager.class.getSimpleName();
  //Must keep this value big than real item, 1000 is big enough
  private static final int ITEM_PER_PAGE = 20;
  private static final RecyclerItemManager sInstance = new RecyclerItemManager();
  private List<RecyclerItem> mAllResults = new ArrayList<>();
  private List<PageHolder> mAllPageHolders = new ArrayList<>();
  private int mCurrentPageIndex;
  private RecyclerItemManager() {
  }
  public static RecyclerItemManager getInstance() {
    return sInstance;
  }
  public int getCurrentPageIndex() {
    return mCurrentPageIndex;
  }
  public void cleanAndAddItems(List<RecyclerItem> source) {
    mAllResults.clear();
    for (RecyclerItem item : source) {
      mAllResults.add(item);
    }
    mAllPageHolders.clear();
    computePages();
  }
  private void computePages() {
    if (mAllResults.size() < ITEM_PER_PAGE) {
      int totalPage = 1;
      List<RecyclerItem> subList = mAllResults;
      mAllPageHolders.add(new PageHolder(totalPage, subList));
    } else {
      int totalPage = computeTotalPage();
      computeItemsOfPage(totalPage);
    }
    showAllPageHolders();
  }
  private void showAllPageHolders() {
    for (PageHolder onePage : mAllPageHolders) {
      Log.d(TAG, "---");
      Log.d(TAG, "PageHolder.getPage():" + onePage.getPage());
      for (RecyclerItem one : onePage.getItems()) {
        Log.d(TAG, "item.getTime():" + one.getTime());
        Log.d(TAG, "item.getDescription():" + one.getDescription());
      }
      Log.d(TAG, "---");
    }
  }
  private int computeTotalPage() {
    int pageCount = mAllResults.size() / ITEM_PER_PAGE;
    int itemsOfEndPage = mAllResults.size() % ITEM_PER_PAGE;
    int totalPage = 0;
    if (itemsOfEndPage == 0) {
      totalPage = pageCount;
    } else {
      totalPage = pageCount + 1;
    }
    return totalPage;
  }
  private void computeItemsOfPage(int totalPage) {
    for (int page = 0; page < totalPage; ++page) {
      int lastPage = totalPage - 1;
      if (page == lastPage) {
        int endElement = mAllResults.size();
        List<RecyclerItem> subList = mAllResults.subList(page * ITEM_PER_PAGE, endElement);
        mAllPageHolders.add(new PageHolder(page, subList));
      } else {
        List<RecyclerItem> subList = mAllResults
            .subList(page * ITEM_PER_PAGE, (page * ITEM_PER_PAGE) + (ITEM_PER_PAGE));
        mAllPageHolders.add(new PageHolder(page, subList));
      }
    }
  }
  public boolean isMorePreviousPage() {
    if (mCurrentPageIndex > 0) {
      return true;
    }
    return false;
  }
  public boolean isMoreNextPage() {
    if (mCurrentPageIndex < mAllPageHolders.size() - 1) {
      return true;
    }
    return false;
  }
  public int decreaseCurrentPageIndex() {
    mCurrentPageIndex--;
    return mCurrentPageIndex;
  }
  public int increaseCurrentPageIndex() {
    mCurrentPageIndex++;
    return mCurrentPageIndex;
  }
  public List<RecyclerItem> getItemsByPage(int page) {
    PageHolder currentPageHolder = mAllPageHolders.get(page);
    return currentPageHolder.getItems();
  }
  public RecyclerItem getItem(int position) {
    PageHolder currentPageHolder = mAllPageHolders.get(mCurrentPageIndex);
    return currentPageHolder.getItems().get(position);
  }
  public void addItem(RecyclerItem item) {
    RecyclerItemDBWrapper.saveBasicRecyclerItem(item);
    cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
  }
  public void isCurrentPageIndexValid() {
    if (mCurrentPageIndex == mAllPageHolders.size()) {
      mCurrentPageIndex--;
    }
  }
  public void deleteItem(int position) {
    RecyclerItem itemForDelete = RecyclerItemManager.getInstance().getItem(position);
    RecyclerItemDBWrapper.deleteItem(itemForDelete);
    RecyclerItemManager.getInstance()
        .cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
    RecyclerItemManager.getInstance().isCurrentPageIndexValid();
  }
  public void modifyItem(int position) {
    RecyclerItem itemForModify = RecyclerItemManager.getInstance().getItem(position);
    itemForModify.setDescription("alread fix");
    itemForModify.setTime("alread fix");
    RecyclerItemDBWrapper.update(itemForModify);
    RecyclerItemManager.getInstance()
        .cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
  }
  private static class PageHolder {
    private int mPage;
    private List<RecyclerItem> mItems;
    public PageHolder(int page, List<RecyclerItem> items) {
      mPage = page;
      mItems = items;
    }
    public int getPage() {
      return mPage;
    }
    public List<RecyclerItem> getItems() {
      return mItems;
    }
  }
}

第11行為設定每頁有多少ItemView的數量。
第12行為Singleton Pattern的寫法(搭配第17~18行,第20~22行)
第13~14行為計算每頁有多少個ItemView用。
Note:第14行的變數較為重要,該List即為每頁的內容 。e.g., 若 list 有3個element,則代表RecyclerView共有3頁,而每個element可以再透過pageHolder.getItems()來取得該頁上的項目。
第15行為表示目前所在頁數。
第17~18行為建構式私有化,目的為不透過建構式讓外界實體化該類別(Singleton Pattern)。
第20~22行為提供外界取得該類別的實體(Singleton Pattern)。
第24~26行取得目前頁數。
第28~36行為重新計算有多少頁,以及每頁的項目。
第38~49行為計算頁數用。基本算法就是藉由ITEM_PER_PAGE來計算每頁的內容。
第51~61行為顯示頁數以及每頁的內容,只是單純用來顯示。
第63~74行計算總頁數。
第76~89行計算每頁的內容。
第91~96為回傳是否還有上一頁。
第98~103為回傳是否還有下一頁。
第105~108行遞減頁數。
第110~113行遞增頁數。
第115~118行取得指定頁數的全部內容。透過mAllPageHolders取得某個element的內容。
第120~123行取得指定頁數的指定內容。透過mAllPageHolders取得某個element的內容再指定某個位置。
第125~128行加入itemview用。首先加入項目到資料庫中再重新刷新頁數及每頁的內容。注意這裡最好也是透過background thread來進行。
第130~134行檢查並更新目前頁數。這個方法比較特別,主要是用於當刪除項目時,若被刪除項目為該頁的最後一項,則目前頁數必須往前移動一頁。
第136~142刪除項目動作。其實作內容為首先從mAllPageHolders取得要刪除項目(第137行),再從資料庫刪除項目(第138行),接著重新刷新list(第139~140行),最後檢查並設定目前頁數是否正確(第141行)。
第144~151行修改項目動作。其實作內容為首先從mAllPageHolders取得要修改的項目(第145行),設定修改內容(第146~147行),再從資料庫更新項目(第148行),接著重新刷新list(第149~150行)。
第153~170行PageHolder為表示某頁及其內容用。mPage為該頁的頁數,mItems為該頁的內容。
最後為RecyclerItem,該類別為基本的資料類別,無邏輯,用來代表RecyclerView上的ItemView。

import net.tsz.afinal.annotation.sqlite.Id;
public class RecyclerItem {
  @Id
  private String id;
  private String listNumber;
  private String time;
  private boolean isLaunch;
  private boolean isVibrator;
  private String description;
  public String getListNumber() {
    return listNumber;
  }
  public void setListNumber(String listNumber) {
    this.listNumber = listNumber;
  }
  public String getTime() {
    return time;
  }
  public void setTime(String time) {
    this.time = time;
  }
  public boolean getIsLaunch() {
    return isLaunch;
  }
  public void setIsLaunch(boolean isLaunch) {
    this.isLaunch = isLaunch;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public boolean getIsVibrator() {
    return isVibrator;
  }
  public void setIsVibrator(boolean vibrator) {
    isVibrator = vibrator;
  }
  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  @Override
  public String toString() {
    return "BasicRecyclerItem{" +
        "listNumber='" + listNumber + '\'' +
        ", time='" + time + '\'' +
        ", isLaunch=" + isLaunch +
        ", isVibrator=" + isVibrator +
        ", description='" + description + '\'' +
        '}';
  }
}

比較需要注意的是第5行透過設定@Id為主鍵。
最重要的package介紹完之後,接著介紹其他協作package,這些package主要提供Application Context的取得以及資料庫的操作。
首先是database package,主要提供資料庫操作,使用的ORM為FinalDB,關於FinalDB參考這裡
包含2個類別(FinalDBwrapper,RecyclerItemDBWrapper)。
FinalDBWrapper

import .common.RecyclerViewSingletonApplication;
import net.tsz.afinal.FinalDb;
public class FinalDBWrapper {
    private static final String TAG = FinalDBWrapper.class.getSimpleName();
    public static final FinalDb DB_INSTANCE = FinalDb
        .create(RecyclerViewSingletonApplication.getInstance(), "test.db", true);
}

相當簡單,主要就是第6~7行的DB_INSTANCE,初始化該變數並設定資料庫名稱,debug模式。後續操作資料庫的動作都是透過DB_INSTANCE來執行。
而RecyclerViewSingletonApplication就是繼承Application,用來取得context。
RecyclerItemDBWrapper

import .basic.RecyclerItem;
import java.util.List;
public class RecyclerItemDBWrapper {
  public static void saveBasicRecyclerItem(RecyclerItem item) {
    FinalDBWrapper.DB_INSTANCE.save(item);
  }
  public static List<RecyclerItem> loadBasicRecyclerItem() {
    return FinalDBWrapper.DB_INSTANCE.findAll(RecyclerItem.class);
  }
  public static void deleteItem(RecyclerItem itemForDelete) {
    FinalDBWrapper.DB_INSTANCE.delete(itemForDelete);
  }
  public static void update(RecyclerItem itemForModify) {
    FinalDBWrapper.DB_INSTANCE.update(itemForModify);
  }
}

這個類別也相當簡單,主要提供資料庫的具體操作,具體操作都是透過FinalDBWrapper DB_INSTANCE 進行。
最後就是common package,只有一個類別RecyclerViewSingletonApplication,主要提供 Application context的取得

import android.app.Application;
public class RecyclerViewSingletonApplication extends Application {
  private static final String TAG = RecyclerViewSingletonApplication.class.getSimpleName();
  private static RecyclerViewSingletonApplication sUniqueInstance;
  public static RecyclerViewSingletonApplication getInstance() {
    return sUniqueInstance;
  }
  @Override
  public void onCreate() {
    super.onCreate();
    sUniqueInstance = this;
    sUniqueInstance.initData();
  }
  private void initData() {
  }
}

 
實際操作畫面


分類
Uncategorized

使用AlarmManager建立定時廣播通知並透過BroadcastReceiver接受廣播通知

1.使用AlarmManager建立或取消定時廣播

首先必須先建立定時廣播的時間

  private static Calendar generateAlarmTime() {
    Calendar reminderTime = Calendar.getInstance();
    reminderTime.setTimeInMillis(System.currentTimeMillis());
    long systemTime = System.currentTimeMillis();
    //同步目前時間
    reminderTime.setTimeInMillis(System.currentTimeMillis());
    // 設定時區避免時差
    reminderTime.setTimeZone(TimeZone.getTimeZone("GMT+8"));
    reminderTime.set(Calendar.HOUR_OF_DAY, 10);
    reminderTime.set(Calendar.MINUTE, 30);
    reminderTime.set(Calendar.SECOND, 0);
    reminderTime.set(Calendar.MILLISECOND, 0);
    long selectTime = reminderTime.getTimeInMillis();
    // 如果現在時間大於設定的時間,就從第2天的設定時間開始
    if (systemTime > selectTime) {
      reminderTime.add(Calendar.DAY_OF_MONTH, 1);
    }
    return reminderTime;
  }

第9~12行即為設定定時廣播的時間,時間為早上10點30分。
時間設定之後便可透過AlarmManager來發送定時廣播。

  private static void setAlarmTime(Context context, Calendar alarmTime) {
    Intent intent = new Intent(context, CycleAlarmReceiver.class);
    PendingIntent pendingIntent = PendingIntent
        .getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
    final int millisecondPerDay = 1000 * 60 * 60 * 24;
    alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, alarmTime.getTimeInMillis(), millisecondPerDay,
        pendingIntent);
  }

這裡需要注意不少地方,第2行CycleAlarmReceiver.class為自定義的Receiver,用途為定時的時間啟動後,該Receiver的onReceiver方法就會被呼叫。
第3~4行需要注意getBroadcaset方法的第2個參數和第4個參數。
第2個參數是用來識別pendingIntent是哪一個,也就是當建立一個定時廣播之後,想要取消掉該廣播或想對該廣播做修改,必須指定是哪一個廣播。以範例來說就是指定0號廣播。
第4個參數表示意義如下
FLAG_CANCEL_CURRENT:如果当前系统中已经存在一个相同的PendingIntent对象,那么就将先将已有的PendingIntent取消,然后重新生成一个PendingIntent对象。
FLAG_NO_CREATE:如果当前系统中不存在相同的PendingIntent对象,系统将不会创建该PendingIntent对象而是直接返回null。
FLAG_ONE_SHOT:该PendingIntent只作用一次。在该PendingIntent对象通过send()方法触发过后,PendingIntent将自动调用cancel()进行销毁,那么如果你再调用send()方法的话,系统将会返回一个SendIntentException。
FLAG_UPDATE_CURRENT:如果系统中有一个和你描述的PendingIntent对等的PendingInent,那么系统将使用该PendingIntent对象,但是会使用新的Intent来更新之前PendingIntent中的Intent对象数据,例如更新Intent中的Extras。
最後是第7行的setRepeat方法,該方法表示這個alarm會重複啟動,而啟動的間隔時間為第3個參數,第3個參數代表每隔一天啟動。
綜合以上2個方法如下

  private static void addReminder(Context context) {
    Calendar alarmTime = generateAlarmTime();
    setAlarmTime(context, alarmTime);
  }

完整類別如下

public class AlarmTimeManager {
  private static Calendar generateAlarmTime() {
    Calendar reminderTime = Calendar.getInstance();
    reminderTime.setTimeInMillis(System.currentTimeMillis());
    long systemTime = System.currentTimeMillis(); //同步目前時間
    reminderTime.setTimeInMillis(System.currentTimeMillis()); // 設定時區避免時差
    reminderTime.setTimeZone(TimeZone.getTimeZone("GMT+8"));
    reminderTime.set(Calendar.HOUR_OF_DAY, 10);
    reminderTime.set(Calendar.MINUTE, 30);
    reminderTime.set(Calendar.SECOND, 0);
    reminderTime.set(Calendar.MILLISECOND, 0);
    long selectTime = reminderTime.getTimeInMillis(); // 如果現在時間大於設定的時間,就從第2天的設定時間開始
      if (systemTime > selectTime)
      {
         reminderTime.add(Calendar.DAY_OF_MONTH, 1);
      }
      return reminderTime;
    }
  private static void setAlarmTime(Context context, Calendar alarmTime) {
    Intent intent = new Intent(context, CycleAlarmReceiver.class);
    PendingIntent pendingIntent = PendingIntent
        .getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
    final int millisecondPerDay = 1000 * 60 * 60 * 24;
    alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, alarmTime.getTimeInMillis(), millisecondPerDay,
        pendingIntent);
  }
   public static void addReminder(Context context)
    {
      Calendar alarmTime = generateAlarmTime();
      setAlarmTime(context, alarmTime);
    }
}

 

2.當setRepeat完成後,alarm時間(早上10點30分)一到便會發送通知給CycleAlarmReceiver。

public class CycleAlarmReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
      Toast.makeText(context, "alarm 啟動", Toast.LENGTH_SHORT).show();
  }
}

onReceive被呼叫後便會顯示一個toast
注意CycleAlarmReceiver必須在AndroidManifest.xml註冊。

    <receiver android:name=".alarm.CycleAlarmReceiver">
    </receiver>

android:name必須指定CycleAlarmReceiver的檔案路徑。

分類
Uncategorized

FinalDB 使用紀錄

FinalDB 為 Android orm 框架其中一種,其最大的好處在於能夠一行完成CRUD操作,這也是當初選擇它的原因,但現在年久失修加上一些缺點,不得不另循其他orm框架(Android Room使用紀錄)。
以下紀錄簡單的使用技巧

1.加入專案相依性

1-1.到 https://github.com/yangfuhai/afinal/tree/master/bin 下載jar檔(afinal_0.5_bin.jar)
1-2.將下載的jar檔放到 App module 的 libs 目錄中
1-3.在App module的build.gradle加入

...
dependencies {
    ...
    implementation files('libs/afinal_0.5_bin.jar')
}

 

2.使用方式

2-1.取得DataBase操作實體

FinalDb.create(context, "test.db", true);

回傳的實體即可進行CRUD操作,context為application context實體,test.db為資料庫名稱,true為啟動debug模式,進行CRUD操作會顯示相關訊息。
以下建立FinalDBWrapper來包裝finalDb

public class FinalDBWrapper {
    public static final FinalDb DB_INSTANCE = FinalDb.create(SingletonApplication.getInstance(), "test.db", true);
}

之後只要透過FinalDBWrapper.DB_INSTANCE來操作即可。
2-2.要儲存的類別

import net.tsz.afinal.annotation.sqlite.Id;
import net.tsz.afinal.annotation.sqlite.Table;
@Table(name = "Reminder")
public class Reminder {
  @Id
  private String listNumber;
  private String time;
  private boolean isLaunch;
  private boolean isVibrator;
  private String description;
  public String getListNumber() {
    return listNumber;
  }
  public void setListNumber(String listNumber) {
    this.listNumber = listNumber;
  }
  public String getTime() {
    return time;
  }
  public void setTime(String time) {
    this.time = time;
  }
  public boolean getIsLaunch() {
    return isLaunch;
  }
  public void setIsLaunch(boolean isLaunch) {
    this.isLaunch = isLaunch;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public boolean getIsVibrator() {
    return isVibrator;
  }
  public void setIsVibrator(boolean vibrator) {
    isVibrator = vibrator;
  }
}

第5行標示資料表名稱。
第8行指定主鍵(不可重複)。
第15~53行為必須有的setter and getter
注意要儲存的類別內無法內嵌另一個自定義類別。e.g.,

public class PatrolReminder {
  ...
  private CustomClass customClass;
}
public class CustomClass{...}

目前似乎還無法這麼做。
2-3. Insert 資料

Reminder reminder = new Reminder();
reminder.setListNumber("0");
FinalDBWrapper.DB_INSTANCE.save(reminder);

第3行插入主鍵為0的reminder,主鍵不可重複。
2-4. Query資料

public static List<Reminder> queryAllReminders() {
    return FinalDBWrapper.DB_INSTANCE.findAll(Reminder.class);
}
public static List<Reminder> queryListNumberEqual20(){
    String searchMessage = " listNumber=\"" + "20" + "\"";
  return  FinalDBWrapper.DB_INSTANCE.findAllByWhere(Reminder.class, searchMessage);
}

第1~3行取得所有已儲存的Reminder
第6~9行取得listNumber 等於20 的 Reminder
2-5. Update資料

  public void update() {
    Reminder reminder = new Reminder();
    reminder.setListNumber("0");
    FinalDBWrapper.DB_INSTANCE.update(reminder);
  }

第3行設定主鍵為0,第4行會以傳入的參數取代已儲存主鍵為0資料。
2-6. Delete資料

  public void delete() {
    Reminder reminder = new Reminder();
    reminder.setListNumber("0");
    FinalDBWrapper.DB_INSTANCE.delete(reminder);
  }

第3行設定主鍵為0,第4行會刪除已儲存主鍵為0資料。

分類
Uncategorized

Room (Persistence Library) 使用紀錄

Room 為 Android 官方推薦的 ORM 框架,該框架封裝了 SQLite 為底層並提供高層介面以方便使用者快速開發數據儲存。
官方連結:https://developer.android.com/topic/libraries/architecture/room
以下紀錄如何安裝及簡單的使用技巧。

1.加入Room 到你的專案中

1-1.在 Project 的 build.gradle 加入

allprojects {
    repositories {
        jcenter()
        google()
    }
}

1-2.在 Module 的 build.gradle 加入

dependencies {
...
    def room_version = "1.1.1"
    implementation "android.arch.persistence.room:runtime:$room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$room_version" // use kapt for Kotlin
...

 

2.建立相關元件

2-1.首先建立要儲存的類別(User)

import android.arch.persistence.room.Embedded;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
@Entity(tableName = "user")
public class User {
  @PrimaryKey
  private int id;
  private int listNumber;
  private boolean active;
  private String name;
  private String description;
  @Embedded
  private Account account;
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public boolean getActive() {
    return active;
  }
  public void setActive(boolean active) {
    this.active = active;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public int getListNumber() {
    return listNumber;
  }
  public void setListNumber(int listNumber) {
    this.listNumber = listNumber;
  }
  public Account getAccount() {
    return account;
  }
  public void setAccount(Account account) {
    this.account = account;
  }
}

注意以下幾點:
第5行使用 @Entity 註解並指定資料表名稱,也可以不指定資料表名稱,預設會使用類別名稱當作資料表名稱。
第8~9行使用 @PrimaryKey 指定主鍵,主鍵為 id
第15~16行自定義類別 Account 必須使用 @Embedded 註明
第18~64行為一般的 getter and setter 方法
自定義類別 Account 如下

import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
@Entity(tableName = "account")
public class Account {
  @PrimaryKey
  @ColumnInfo(name = "account_id")
  private int id;
  @ColumnInfo(name = "account_name")
  private String name;
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
}

注意以下幾點
和 User 類別類似,唯一需要注意的是第 9 行和第 12 行的 @ColumnInfo() 註解。
當某個類別(Account)為另一個類別(User)的嵌入類別時,若欄位名稱有相同必須透過 @ColumnInfo 指定另一個名稱。
以第 9 行 name = “account_id” 為例,就是告訴 Room,Account 的 id 在資料表中欄位名稱為 account_id,避免和 User 的 id 欄位名稱重覆。

2-2.建立DAO(UserDAO)

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
@Dao
public interface UserDao {
  @Query("SELECT * FROM user")
  public List<User> getAllUser();
  @Query("SELECT * FROM user WHERE active = :active")
  public List<User> getUserActive(boolean active);
  @Query("SELECT * FROM user WHERE listNumber < :lessNumber")
  public List<User> getUserListNumberLessThan(int lessNumber);
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  public void insertUser(User users);
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  public void insertUsers(List<User> users);
  @Update
  public void updateUsers(User... users);
  @Update
  public void updateUser(User user);
  @Delete
  public void deleteUser(User user);
  @Delete
  public void deleteUsers(List<User> users);
}

UserDAO 即為操作 User 的 Data Access Object,關於 User 的資料庫操作介面全部都會定義在這裡。只需要定義介面就好,Room 會自動實作其內容。
注意以下幾點
第9~10行使用建立 interface 並使用 @Dao 標示
第12~13行使用 @Query 標示該介面為 query 動作並在括號中指定要動作的內容。
Room 提供了4種註解@Query, @Update, @Insert, @Delete 這4種註解分別提供不同的動作,各個動作可以指定的內容請參考 API。
以第12~13行為例,呼叫 getAllUser 就等於 select * from user 代表回傳已儲存的所有 user 內容。
第15~16行代表回傳已儲存的所有 user 內容且 active 欄位必須符合傳入的參數(true or false)。
第18~19行代表回傳已儲存的所有 user 內容且 listnumber 欄位必須小於指定的參數。
第21~22行為插入資料用,而 onConflict = OnConflictStrategy.REPLACE 代表若主鍵發生重複,那就把新的資料取代已儲存的資料。
第24~25行為插入資料用,類似第21~22行。差別在於可以傳入 list。
第27~28行為更新資料用,會根據主鍵以傳入的資料取代已儲存的資料。
第30~31行為更新資料用,類似第27~28行。差別在於可以傳入 list。
第33~34行為刪除資料用,會根據主鍵刪除已儲存的資料。
第36~37行為刪除資料用,類似第33~34行。差別在於可以傳入 list。

2-3.建立一個抽象類別並繼承RoomDatabase

@Database(version = 1, entities = {User.class})
public abstract class RoomDBWrapper extends RoomDatabase {
  private static RoomDBWrapper sUnique = Room
      .databaseBuilder(SingletonApplication.getInstance(), RoomDBWrapper.class, "user.db")
      .allowMainThreadQueries().build();
  public static RoomDBWrapper getInstance() {
    return sUnique;
  }
  public abstract UserDao getUserDao();
}

注意以下幾點
1.該類別主要用途為建立資料庫並管理 DAO,可以考慮套用 Singleton Pattern。
2.必須是抽象類別並繼承 RoomDatabase
第1行使用 @Database 註解並指定 version(版本),entities(要儲存類別,如有多個就必須一一加入)
第4行建立 sUnique 變數,之後都透過該變數來取得 DAO 並進行相關操作。
第5行的 SingletonApplication 是自定義類別,繼承 Application 如下

public class SingletonApplication extends Application
{
    private static SingletonApplication sUniqueInstance;
    public static SingletonApplication getInstance()
    {
        return sUniqueInstance;
    }
...

user.db 為資料庫名稱。
第6行注意 allowMainThreadQueries(),呼叫該方法代表予許操作資料庫的動作進行在 ui thread 上,若沒有呼叫該方法,預設是不能在 ui thread 上操作資料。其實也不建議在 ui thread 上進行資料庫操作,避免 ANR。
第8~10行為提供對外存取 sUnique 的方法。
第12行為要儲存類別的 DAO。

3.使用方式

3-1.Insert 資料

  private void insert() {
    for (int i = 0; i < 10; ++i) {
      User user = new User();
      user.setId(i);
      user.setListNumber(i);
      user.setName("name " + i);
      if (i % 2 == 0) {
        user.setActive(true);
      } else {
        user.setActive(false);
      }
      user.setDescription("empty");
      Account account = new Account();
      account.setId(i);
      account.setName("name " + i);
      user.setAccount(account);
      RoomDBWrapper.getInstance().getUserDao().insertUser(user);
    }
  }

第3~16行為建立要 insert 的資料。
第17行即呼叫 RoomDBWrapper 進行實際 insert 的動作。
insert 方法完成後 user 資料表就會有 10 筆 user 資料。

3-2.Query 資料

  private void queryAllUsers() {
    List<User> allUser = RoomDBWrapper.getInstance().getUserDao().getAllUser();
  }

3-3.Update資料

  private void updateUser5() {
    User user = new User();
    user.setId(5);
    user.setName("updateUser5");
    RoomDBWrapper.getInstance().getUserDao().updateUser(user);
  }
  private void updateUsers() {
    for (int i = 0; i < 10; ++i) {
      User user = new User();
      user.setId(i);
      user.setName("name " + i + 10);
      RoomDBWrapper.getInstance().getUserDao().updateUsers(user);
    }
  }

第3行當 user.setId(5) 之後,並將該 user 傳入第4行 updateUser(user)方法後,就會取代已儲存資料中主鍵(id)為 5 的項目。
第11行 setId 的參數範圍從 0~9,當呼叫第13行 updateUsers(user) 後,就會取代已儲存資料中主鍵(id)範圍從 0~9 的項目。

3-4. Delete 資料

  private void deleteUser2() {
    User user = new User();
    user.setId(2);
    RoomDBWrapper.getInstance().getUserDao().deleteUser(user);
  }

第3行指定 setId(2)
第4行呼叫 deleteUser(user),即代表刪除已儲存資料中,主鍵(id)為 2 的項目。

分類
Uncategorized

android 裝置開機後收到廣播通知(BootUpReceiver)

一般要收到系統的廣播通知都是類似的做法。
1.首先建立要接收系統開機完成廣播通知的Receiver

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class BootUpReceiver extends BroadcastReceiver {
  private static final String TAG = BootUpReceiver.class.getSimpleName();
  @Override
  public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "boot up!!!", Toast.LENGTH_SHORT).show();
  }
}

onReceive方法中即為收到通知後要做的動作。
2.在AndroidManifest.xml新增開機通知權限以及BootUpReceiver的註冊。

...
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
...
    <receiver
      android:enabled="true"
      android:exported="true"
      android:name=".alarm.BootUpReceiver">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
    </receiver>
...

要注意android:name必須對應BootUpReceiver的檔案路徑。

分類
Uncategorized

使用 RingtoneManager 播放提示音(NOTIFICATION),鬧鐘(ALARM),來電鈴聲(RINGTONE)

1.播放預設的提示音

 public static void playDefaultNotification(Context context) {
    Uri defaultNotificationURI = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
    Ringtone soundOfDefaultNotification = RingtoneManager.getRingtone(context, defaultNotificationURI);
    soundOfDefaultNotification.play();
  }

其中RingtoneManager.TYPE_NOTIFICATION為提示音的設定。
若想播放預設的鬧鐘只要改為RingtoneManager.TYPE_ALARM
播放預設的鬧鐘只要改為RingtoneManager.TYPE_RINGTONE
2.播放裝置內其他的提示音
2-1.首先取得裝置內的提示音清單

 public static Map<String, String> getSoundTitleAndUri(Context context, int type) {
    RingtoneManager manager = new RingtoneManager(context);
    manager.setType(type);
    Cursor cursor = manager.getCursor();
    Map<String, String> titleAndUri = new HashMap<>();
    while (cursor.moveToNext()) {
      String title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
      String id = cursor.getString(RingtoneManager.ID_COLUMN_INDEX);
      String uri = cursor.getString(RingtoneManager.URI_COLUMN_INDEX);
      titleAndUri.put(title, uri+"/"+id);
    }
    return titleAndUri;
  }

使用的方式如下(取得提示音的清單)

getSoundTitleAndUri(context, RingtoneManager.TYPE_NOTIFICATION);

回傳的Map其key值即為提示音的名稱,value值為提示音的Uri。
若想取得的是鬧鐘清單,只要改為RingtoneManager.TYPE_ALARM即可
若想取得的是來電鈴聲清單,只要改為RingtoneManager.TYPE_RINGTONE即可
若把該Map印出來如下。

key:Alloy value:content://media/internal/audio/media/89
key:Arc value:content://media/internal/audio/media/90
key:Bezel value:content://media/internal/audio/media/91
...

Alloy為提示音的名稱,其URI為 content://media/internal/audio/media/89
最後要播放指定的提示音如下

 public static void playSound(Context context, String uri) {
    Uri soundUri = Uri.parse(uri);
    Ringtone sound = RingtoneManager.getRingtone(context, soundUri);
    sound.play();
  }

使用的方法為

playSound(context,"content://media/internal/audio/media/89");//play Alloy
分類
Uncategorized

Android Get MIME Type and open File by Intent

取得MIME之後再搭配Intent即可根據MIME Type開啟應用程式。
public static String getMIMEType(Uri uri)
{
String mimeType = null;
if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
mimeType = SingletonApplication.getInstance().getContentResolver().getType(uri);
} else {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
MimeTypeMap.getFileExtensionFromUrl(uri.toString()).toLowerCase());
}
return mimeType;
}
注意參數Uri 的使用如下
File externalStorageDirectoryFilePath = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),”/readme.docx”);
Log.d(TAG,”File MIME:”+getMIMEType(Uri.fromFile(externalStorageDirectoryFilePath));
其中 externalStorageDirectoryFilePath.getAbsolutePath()) 為回傳外部儲存裝置的目錄路徑,
參數Environment.DIRECTORY_DOCUMENTS為指定回傳外部儲存裝置的目錄路徑的
Documents目錄路徑。因此externalStorageDirectoryFilePath.getAbsolutePath()); 回傳路徑如下
/storage/emulated/0/Documents/readme.docx
docx的MIME為
application/vnd.openxmlformats-officedocument.wordprocessingml.document
txt的MIME為
text/plain
接著使用Intent開啟檔案
File externalStorageDirectoryFilePath = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),”/readme.docx”);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(externalStorageDirectoryFilePath,
getMIMEType(Uri.fromFile(externalStorageDirectoryFilePath)));
activity.getContext().startActivity(intent);

分類
Uncategorized

Android layout支援多尺寸螢幕

詳細內容參考官方網站 https://developer.android.com/guide/practices/screens_support.html
以下紀錄簡略記錄。
若是只有1個layout file,Android自動縮放layout上的UI元件來搭配目前運行的裝置。
但因為不同裝置之間的螢幕尺寸差異太大,即使經過自動縮放,UI元件的外觀或位置也會不正常。如5吋手機和10吋平板使用同1個layout file。
因此比較好的方式為根據不同螢幕尺寸加入不同的layout file,讓android自動判斷套用不同layout file。
實作方式為在res目錄下增加相對應的目錄,layout-small,layout-normal,layout-large,layout-xlarge 各目錄會支援不同的螢幕尺寸,然後在各個目錄中新增並調整layout file 的元件。
e.g.
目前必須支援6吋手機以及10吋平板,經過實驗,android 在6吋手機上會套用layout-normal目錄中的layout file,而10吋會套用 layout-xlarge目錄的layout file。
因此新增layout-normal以及layout-xlarge目錄,並放置對應的layout file(activity_main.xml)
最後在到各個activity_main.xml去調整UI。

分類
Uncategorized

Vrapper plugin hotkey

ref: http://vrapper.sourceforge.net/documentation/?topic=optional_plugins#ipmotion
1.vil -> 選擇該行,不包括空白。
2.via -> 選擇argument。
3.vif -> 選擇函式內容。
4.vii -> 選擇縮排。
5.gcc -> 註釋
6.gd -> 移動到宣告處
7.gR -> 重新命名(重構)
8.gr -> 顯示重構選單
9.gm -> 顯示source選單
10. :fm -> format