只在此山中,雲深不知處


聽首歌



© 2018 by Shawn Huang
Last Updated: 2018.5.27

Introduction

因為版本問題,重新安裝了三次才能正常使用,使用的build.gradle(Module:app)中的dependencies如下。
build.gradle(Module:app) -- (compileSdkVersion 26;targetSdkVersion 26;)
dependencies {
	implementation fileTree(dir: 'libs', include: ['*.jar'])
	implementation 'com.android.support:appcompat-v7:26.1.0'
	implementation 'com.android.support.constraint:constraint-layout:1.1.2'
	testImplementation 'junit:junit:4.12'
	androidTestImplementation 'com.android.support.test:runner:1.0.2'
	androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}	
需要先施作File > Sync Project with Gradle Files才能使用。

Widget

手機程式類似視窗程式設計,widget為其中的元件,例如之前顯示Hello World的元件即為一個元件(TextView)。在activity_mail.xml下,顯示手機的左手邊有兩個小視窗,上方為Palette,下方為Component Tree,Palette內含可以使用的widget,如下圖:


而Component Tree則內含目前使用的widget及其關係,如下圖:


在hello這個TextView上點一下,發現手機畫面右方的Attributes視窗有變化,會變成如下。


Button

接下來介紹Button元件。在Palette上點Button(common > Button or Buttons > Button),拖曳到手機任意位置上,在Attributes的四方框四面的+號上點開邊界。修改ID(btnGreeting)與顯示文字(GREETING),將按鈕調整到你想要的位置並修改其屬性(文字大小背景顏色等)。


設計好了外觀,接下來切換到MainActivity.java,我們需要在此設計按鈕被按下後產生甚麼效果。因為設計程式時會需要import相關package,要記住所有package的名稱太麻煩,所以先設定自動import。做法是選擇File>Settings...,在跳出來的視窗中找到Editor>General>Auto Import,並將各選項設定如以下畫面。



AppApplication1
package com.mtjade.appapplication1;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

	private Button btnGreeting; // declare Button
	private TextView tvHello; // declare TextView

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		btnGreeting = (Button) findViewById(R.id.btnGreeting); // cast view to a button
		tvHello = (TextView) findViewById(R.id.hello); // cast view to a textview

		btnGreeting.setOnClickListener(new View.OnClickListener() { // event listener
			@Override
			public void onClick(View view) {
				tvHello.setText("What's up!");
			}
		});// end of event listener

	}
}	

Text > Plain Text


當要設計一個需要輸入數值的程式,例如計算BMI,我們需要可以接受使用者輸入的元件,選擇使用Text > Plain Text。在Palette選擇Text > Plain Text,置於適當的位置。設計計算BMI的程式需要輸入身高與體重,所以需要兩個Plain Text。而顯示計算後的結果需要一個TextView(common > TextView or Text > TextView),此外,尚需要一個Button來觸發計算。所以可以配置如下圖。



AppApplication1
package com.mtjade.appapplication1;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

	private Button btnBMI;
	private TextView tvBMI;
	private EditText txtHeight;
	private EditText txtWeight;

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

		btnBMI = (Button) findViewById(R.id.btnBMI);
		tvBMI = (TextView) findViewById(R.id.tvBMI);
		txtHeight = (EditText) findViewById(R.id.txtHeight);
		txtWeight = (EditText) findViewById(R.id.txtWeight);

		btnBMI.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				double h = Double.parseDouble(txtHeight.getText().toString().trim());
				double w = Double.parseDouble(txtWeight.getText().toString().trim());
				double bmi = w/h/h;

				if(bmi < 18.5)
					tvBMI.setText("BMI = "+bmi+"\n太輕了,要多吃一點");
				else if(bmi>=18.5 && bmi<23.9)
					tvBMI.setText("BMI = "+bmi+"\n標準身材,請好好保持");
				else if(bmi>=23.9 && bmi<27.9)
					tvBMI.setText("BMI = "+bmi+"\n喔喔!得控制一下飲食了,請加油!");
				else
					tvBMI.setText("BMI = "+bmi+"\n肥胖容易引起疾病,得要多多注意自己的健康囉!");
			}
		});
	}
}

Activities


AppActivities > MainActivity.java
package com.mtjade.appactivities;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		// Button Next
		Button btnNext = (Button) findViewById(R.id.btnNext);
		btnNext.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				Intent intent = new Intent(getApplicationContext(), secondActivity.class);
				startActivity(intent);
			}
		});
	}
}

Intent

除了可以轉換到另一個Activity,還可以在轉換過程中傳遞資訊。首先先修改在MainActivity.java內的onClick()方法如下。
MainActivity.java
public void onClick(View view) {
	Intent intent = new Intent(getApplicationContext(), secondActivity.class);
	intent.putExtra("MtJade.com", "Hi, there.");
	startActivity(intent);
}	
接下來修改secondActivity.java的程式碼如下。
接下來修改secondActivity.java
package com.mtjade.appactivities;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class secondActivity extends AppCompatActivity {

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

		if(getIntent().hasExtra("MtJade.com")){
			TextView tv = (TextView) findViewById(R.id.textView);
			String s = getIntent().getExtras().getString("MtJade.com");
			tv.setText(s);
		}
	}
}	

Activities outside App


除了可以轉換到APP的另一頁,還可以到其他網頁,首先先在activity_main.xml再加上一個按鈕,假定按下按鈕要轉換到Google,所以按鈕文字設為Google,ID便設為btnGoogle。接下來在MainActivity.java加上以下的程式碼。
MainActivity.java
// Button Google
Button btnGoogle = (Button) findViewById(R.id.btnGoogle);
btnGoogle.setOnClickListener(new View.OnClickListener() {
	@Override
	public void onClick(View view) {
		String gg = "http://www.google.com";
		Uri uriGg = Uri.parse(gg);

		Intent intentGg = new Intent(Intent.ACTION_VIEW, uriGg);
		if(intentGg.resolveActivity(getPackageManager()) != null){
			startActivity(intentGg);
		}
	}
});

Containers -- ListView

ListView似乎在新版將被淘汰,不過還是值得玩一下,找到Palette > legacy > ListView,拖曳至手機畫面。在左邊檔案目錄中找到Android > app > res > values > strings.xml,在其中加入以下內容。
strings.xml
<resources>
	<string name="app_name">AppListView</string>

	<string-array name="cars">
		<item>Mercedes</item>
		<item>Porsche</item>
		<item>Maserati</item>
	</string-array>

	<string-array name="prices">
		<item>6520000</item>
		<item>6080000</item>
		<item>3990000</item>
	</string-array>

	<string-array name="descriptions">
		<item>Mercedes-AMG GT.</item>
		<item>911 Carrera Cabriolet</item>
		<item>Ghibli</item>
	</string-array>
</resources>	
接下來在左方檔案目錄中的Android > app > res > layout上點右鍵,打開New > Layout resource file,會出現以下視窗:


修改紅色方框內的名稱後按OK。多了一個名為listviewdetails.xml的tab,可以在Attributes處修改textSize(e.g. 24sp),讓顯示的文字大一些。回到MainActivity.java,修改程式碼如下:
AppListView > MainActivity.java
package com.mtjade.applistview;

import android.content.res.Resources;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {

	ListView listView;
	String cars[];
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		Resources res = getResources();
		listView = findViewById(R.id.listView);

		cars = res.getStringArray(R.array.cars);
		listView.setAdapter(new ArrayAdapter<>(this, R.layout.listviewdetails, cars));
	}
}	

More


適才的例子僅為測試,只有出現車名。現在將listviewdetails.xml刪除,按右鍵選擇delete...。重新建立listviewdetails.xml,一樣在左方檔案目錄中的Android > app > res > layout上點右鍵,打開New > Layout resource file,File name一樣使用listviewdetails,但是Root element改為RelativeLayout,按OK。在出現的listviewdetails.xml的手機畫面上配置三個TextView如下,其ID分別為car, price, description。


接下來增加一個java Class,在Android > app > java > ...applistview > MainActivity按右鍵(在...applistview or MainActivity皆可)點選New > Java Class,出現視窗如下:


修改Name --> carAdapter,Superclass --> BaseAdapter。>> OK。接下來到carAdapter.java修改程式碼如下。
carAdapter.java
package com.mtjade.applistview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

public class carAdapter extends BaseAdapter {

	LayoutInflater mInflater;
	String cars[];
	String prices[];
	String descriptions[];

	public carAdapter(Context c, String[] cars, String[] prices, String[] descriptions) {
		this.cars = cars;
		this.prices = prices;
		this.descriptions = descriptions;
		mInflater = (LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}

	@Override
	public int getCount() {
		return cars.length;
	}

	@Override
	public Object getItem(int i) {
		return cars[i];
	}

	@Override
	public long getItemId(int i) {
		return i;
	}

	@Override
	public View getView(int i, View view, ViewGroup viewGroup) {
		View v = mInflater.inflate(R.layout.listviewdetails, null);
		TextView tvCar = (TextView) v.findViewById(R.id.car);
		TextView tvPrice = (TextView) v.findViewById(R.id.price);
		TextView tvDescription = (TextView) v.findViewById(R.id.description);

		String car = cars[i];
		String price = "$" + prices[i];
		String description = descriptions[i];

		tvCar.setText(car);
		tvPrice.setText(price);
		tvDescription.setText(description);

		return v;
	}
}
再到MainActivity.java修改程式碼如下。
MainActivity.java
package com.mtjade.applistview;

import android.content.res.Resources;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {

	ListView listView;
	String cars[];
	String prices[];
	String descriptions[];
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		Resources res = getResources();
		listView = findViewById(R.id.listView);

		cars = res.getStringArray(R.array.cars);
		prices = res.getStringArray(R.array.prices);
		descriptions = res.getStringArray(R.array.descriptions);
		//listView.setAdapter(new ArrayAdapter<>(this, R.layout.listviewdetails, cars));

		carAdapter ca = new carAdapter(this, cars, prices, descriptions);
		listView.setAdapter(ca);
	}
}

Add Activities


接下來回到MainActivity.java,在剛才的setAcapter()方法後面,加上以下程式碼。
MainActivity.java
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
	@Override
	public void onItemClick(AdapterView&lt;?> adapterView, View view, int i, long l) {
		Intent showDetailsActivity = new Intent(getApplicationContext(), DetailsActivity.class);
		showDetailsActivity.putExtra("MtJade.com", i);
		startActivity(showDetailsActivity);
	}
});

Add Photos


現在點任一個item看到的圖都是同一個,要改變圖案,首先先準備三張圖片,將此三張圖片放置於Android > app > res > drawablea內。接著到DetailsActivity.java修改程式碼。
DetailsActivity.java
package com.mtjade.applistview;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Display;
import android.widget.ImageView;

public class DetailsActivity extends AppCompatActivity {

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

		Intent in = getIntent();
		int index = in.getIntExtra("MtJade.com", -1);

		if(index > -1){
			int pic = getImg(index);
			ImageView img = (ImageView) findViewById(R.id.imageView);
			scaleImg(img, pic);
		}
	}

	private int getImg(int index){
		switch(index){
			case 0: return R.drawable.benz;
			case 1: return R.drawable.maserati;
			case 2: return R.drawable.porsche;
			default: return -1;
		}
	}
	private void scaleImg(ImageView img, int pic){
		Display screen = getWindowManager().getDefaultDisplay();
		BitmapFactory .Options options = new BitmapFactory.Options();

		options.inJustDecodeBounds = true;
		BitmapFactory.decodeResource(getResources(), pic, options);

		int imgWidth = options.outWidth;
		int screenWidth = screen.getWidth();
		if(imgWidth > screenWidth){
			int ratio = Math.round((float)imgWidth/(float)screenWidth);
			options.inSampleSize = ratio;
		}
		options.inJustDecodeBounds = false;
		Bitmap scaledImg = BitmapFactory.decodeResource(getResources(), pic, options);
		img.setImageBitmap(scaledImg);
	}
}

Data Persistence

之前介紹過的Intent可以轉換到另一頁並傳遞資料(Bundle),另外還有其他方式可以儲存資料並傳輸。例如:

SharedPreferences

大概是最簡單的方式,使用SharedPreferences物件。開啟新的專案,在其上設計兩個Plain text(用以輸入first name與last name),然後在之下設計兩個按鈕,第一個顯示Save,另一個顯示Load,此外,在其下加上一個TextView用以顯示文字。如下: Java code如下:
Test > Main3Activity.java
package com.mtjade.test;

import android.content.Context;
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class Main3Activity extends AppCompatActivity {

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

		Button btSave = (Button) findViewById(R.id.btSave);
		Button btLoad = (Button) findViewById(R.id.btLoad);
		final EditText txtFirstName = (EditText) findViewById(R.id.txtFirstName);
		final EditText txtLastName = (EditText) findViewById(R.id.txtLastName);
		final TextView tvDisplay = (TextView) findViewById(R.id.tvDisplay);

		btSave.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				SharedPreferences sp = getPreferences(Context.MODE_PRIVATE);
				SharedPreferences.Editor edit = sp.edit();
				String fname = txtFirstName.getText().toString();
				String lname = txtLastName.getText().toString();
				edit.putString("lname", lname);
				edit.putString("fname", fname);
				edit.apply();
				Toast.makeText(Main3Activity.this, "Saved", Toast.LENGTH_SHORT).show();
			}
		});

		btLoad.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				SharedPreferences sp = getPreferences(Context.MODE_PRIVATE);
				String fname = sp.getString("fname", "na");
				String lname = sp.getString("lname", "na");
				tvDisplay.setText(String.format("%s, %s", fname, lname));
			}
		});
	}
}	

getSharedPreferences


上述的例子僅能在同一個Activity內操作,若是要在另一個Activity使用資料,則方式如下。首先設計另一個Activity,其中包含一個按鈕與一個TextView,按鈕用來觸發取得資料,TextView用來顯示資料(當然也可以利用Toast等其他方式顯示資料)。接下來將上例中的Save內的onClick事件中的SharedPreferences sp = getPreferences(Context.MODE_PRIVATE);這一行comment掉,修改如下:
Test > Main3Activity.java
String file = "myFile1";
SharedPreferences sp = getSharedPreferences(file, Context.MODE_PRIVATE);
接下來在另一個Activity內設計onClick事件如下:
Button btShow = (Button) findViewById(R.id.btShow);
final TextView tvPreferences = (TextView) findViewById(R.id.tvPreferences);
btShow.setOnClickListener(new View.OnClickListener() {
	@Override
	public void onClick(View view) {
		SharedPreferences sp = getSharedPreferences("myFile1", Context.MODE_PRIVATE);
		String fname = sp.getString("fname", "na");
		String lname = sp.getString("lname", "na");
		tvPreferences.setText(fname + " " + lname);
	}
});

Internal Storage

直接將資料檔案儲存在手機內存內,使用openFileOutput(filename, Context.MODE_APPEND)來開啟輸入資料流(write),或使用openFileInput(filename)來開啟讀取資料流(read),範例的元件配置為設置一個editText(可以輸入多行資訊),兩個按鈕(一顯示Save,一顯示Load),最後再設置一個TextView用以顯示資料。
Test > Main4Activity.java
package com.mtjade.test;

import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import org.w3c.dom.Text;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main4Activity extends AppCompatActivity {

	EditText editText;
	private String filename = "myfile.txt";
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main4);

		editText = (EditText) findViewById(R.id.etInput);
		Button btSave = (Button) findViewById(R.id.btSave);
		btSave.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				saveData(view);
				Toast.makeText(Main4Activity.this, "Saved", Toast.LENGTH_SHORT).show();
			}
		});
		Button btLoad = (Button) findViewById(R.id.btLoad);
		btLoad.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				loadData(view);
			}
		});
	}

	public void saveData(View v){
		String str = editText.getText().toString();
		FileOutputStream out = null;
		try {
			out = openFileOutput(filename, Context.MODE_APPEND); // may throw a “FileNotFoundException”;
			out.write(str.getBytes()); // It can only work with bytes, may throw an “IOException”;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e){
			e.printStackTrace();
		} finally{
			if (out!=null){
				try{
					out.close(); // may throw an “IOException”
				}catch(IOException e){
					e.printStackTrace();
				}
			}
		}
	}

	public void loadData(View v) {
		TextView tvShow = (TextView) findViewById(R.id.tvShow);
		FileInputStream in = null;
		StringBuilder sb = new StringBuilder();

		try {
			in = openFileInput(filename); // may throw a “FileNotFoundException”;
			int read = 0;
			while((read = in.read())!=-1) { // when it reaches the end of the file, it will return -1
				sb.append((char) read);// in.read( ) method returns an int;
			}
			tvShow.setText(sb.toString());
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(in!=null){
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}	

Room

使用Room來溝通資料庫。首先將以下程式碼加入到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

// optional - RxJava support for Room
implementation "android.arch.persistence.room:rxjava2:$room_version"

// optional - Guava support for Room, including Optional and ListenableFuture
implementation "android.arch.persistence.room:guava:$room_version"

// Test helpers
testImplementation "android.arch.persistence.room:testing:$room_version"

Gestures

除了使用按鈕等元件來觸發,也可以使用手勢來操控手機。使用的類別為GestureDetectorCompat,以下示範如何左右滑動來轉換Activity(換頁)。
Test > Main4Activity.java
@Override
public boolean onTouchEvent(MotionEvent event){
	this.mDetector.onTouchEvent(event);
	return super.onTouchEvent(event);
}
class MyGestureListener extends GestureDetector.SimpleOnGestureListener{

	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY){
		if(e2.getX() - e1.getX() > FLIP_DISTANCE){
			Intent intent = new Intent(Main4Activity.this, Main3Activity.class);
			startActivity(intent);
			return true;
		}
		return false;
	}
}

Network

Under Construction!