本文已授權(quán)公眾號hongyangAndroid原創(chuàng)首發(fā)
最近公司產(chǎn)品大大說我們需要一個動態(tài)替換的閃屏頁面茂嗓,like 某貓,某東一樣庶柿,可以動態(tài)替換挤巡。
產(chǎn)品大大就是厲害,說一句話我們就需要實現(xiàn)好幾個功能:
- 創(chuàng)建一個冷啟動后的閃屏頁面(Splash 頁面)
- 這個頁面默認 3s 倒計時盛末,點擊倒計時按鈕可以跳轉(zhuǎn)并結(jié)束倒計時
- 點擊圖片如果有外鏈弹惦,則跳轉(zhuǎn)應(yīng)用的 web 頁面用來作為活動頁面(沒錯這點和某貓很像)
- 動態(tài)替換厲害了否淤,我們需要在進入這個頁面后去后臺請求一下是否有新的圖片,如果是新的圖片則下載到本地棠隐,替換掉原來的圖片石抡,下次用戶在進入 Splash 就會看到一個嶄新的圖片。
一助泽、布局實現(xiàn)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/sp_bg"
android:src="@mipmap/icon_splash"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<Button
android:visibility="invisible"
android:gravity="center"
android:textSize="10sp"
android:textColor="@color/white"
android:id="@+id/sp_jump_btn"
android:background="@drawable/btn_splash_shape"
android:layout_width="60dp"
android:layout_height="30dp"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp"/>
</RelativeLayout>
布局文件文件相對來說還是比較簡單啰扛,就需要一個 ImageView 和 Button 即可,Button 的背景是一個自定義的 shape嗡贺,透明度顏色啥的,根據(jù)UI妹砸說的算就好了侠讯。
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#99c4c4c4"/>
<corners android:radius="20dp"/>
<stroke
android:width="0.7dp"
android:color="#7fffffff"/>
</shape>
二、倒計時功能實現(xiàn)
實現(xiàn)倒計時的功能方法有很多暑刃,最基本的你可以使用 Handler 來實現(xiàn)吧厢漩,還可以是用 Timer 吧。
但是由于之前寫驗證碼倒計時的時候發(fā)現(xiàn) android.os 中有一個神奇的類叫 CountDownTimer 的類岩臣,此類神奇之處就在于你完全不需要理會那些線程交互他都給你處理好了溜嗜,你只管在回調(diào)中處理時間設(shè)置跳轉(zhuǎn)邏輯就好了。
但是有一個不足的地方就它的第一秒的倒計時有時候會不可見架谎,所以我們將倒計時總時間設(shè)置為 3200ms 炸宵。
private CountDownTimer countDownTimer = new CountDownTimer(3200, 1000) {
@Override
public void onTick(long millisUntilFinished) {
mSpJumpBtn.setText("跳過(" + millisUntilFinished / 1000 + "s)");
}
@Override
public void onFinish() {
mSpJumpBtn.setText("跳過(" + 0 + "s)");
gotoLoginOrMainActivity();
}
};
最后需要在有閃屏頁面的情況下,進入開啟倒計時:
private void startClock() {
mSpJumpBtn.setVisibility(View.VISIBLE);
countDownTimer.start();
}
三谷扣、下載功能實現(xiàn)點擊跳轉(zhuǎn)功能實現(xiàn)
上邊說了我們 APP 點擊圖片需要可以跳轉(zhuǎn)土全,下面代碼給出了背景點擊跳轉(zhuǎn)的邏輯:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
ButterKnife.bind(this);
checkSDCardPermission();
}
@OnClick({R.id.sp_bg, R.id.sp_jump_btn})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.sp_bg:
gotoWebActivity();
break;
case R.id.sp_jump_btn:
gotoLoginOrMainActivity();
break;
}
}
跳轉(zhuǎn)邏輯可以根據(jù)實際的項目需求來規(guī)定,下面的代碼中 Splash 為本地序列化的 model 用來存儲網(wǎng)絡(luò)下載的閃屏頁面信息会涎,稍后會有詳細的序列化過程裹匙,此刻我們只需要關(guān)注跳轉(zhuǎn)邏輯:
private Splash mSplash;
private void gotoWebActivity() {
if (mSplash != null && mSplash.click_url != null) {
Intent intent = new Intent(this, BannerActivity.class);
intent.putExtra("url", mSplash.click_url);
intent.putExtra("title", mSplash.title);
intent.putExtra("fromSplash", true);
intent.putExtra("needShare", false);
startActivity(intent);
finish();
}
}
機智的你可能看出來我們并沒有在離開頁面的時候結(jié)束掉 timer,其實我們是復(fù)寫了 onDestroy 方法末秃。
@Override
protected void onDestroy() {
super.onDestroy();
if (countDownTimer != null)
countDownTimer.cancel();
}
其實跳轉(zhuǎn)以后還有一個坑就是概页,從 web 頁面返回的時候,因為閃屏頁面是你應(yīng)用的第一個頁面练慕,而跳轉(zhuǎn)到 web 頁面的是你 finish 掉了該頁面惰匙,那么從 web 頁返回的時候不做處理,用戶就直接退出了 app 這樣當(dāng)然是不允許的铃将。
所以請在 web 頁面中添加以下邏輯:
//此方法是toolbar 的返回事件調(diào)用的方法 mFromSplash 為啟動頁面?zhèn)鬟f過來的參數(shù)
@Override
protected void onLeftClick(View view) {
if (mFromSplash) {
gotoLoginOrMainActivity();
} else {
super.onLeftClick(view);
}
}
// 此方法為系統(tǒng)返回鍵的監(jiān)聽
@Override
public void onBackPressed() {
if (mWebView.canGoBack()) {
mWebView.goBack();
} else if (mFromSplash) {
gotoLoginOrMainActivity();
} else {
super.onBackPressed();
}
}
// 下面是跳轉(zhuǎn)邏輯
private void gotoLoginOrMainActivity() {
if (UserCenter.getInstance().getToken() == null) {
gotoLoginActivity();
} else {
gotoMainActivity();
}
}
.... gotoLoginActivity项鬼,gotoMainActivity 太長了,不給了自己寫 (*^__^*) 嘻嘻……
四劲阎、下載網(wǎng)絡(luò)圖片以及序列化本地
上邊說了我們有這樣一個需求绘盟,就是如果后臺的接口返回的圖片與本地序列化的圖片不同,我們需要將新的圖片下載到本地,然后下次進入 Splash 的時候就展示的新的圖片了奥此。
這里你需要知道知識有下邊幾個:
- java bean 序列化與反序列化的知識
- IntentService 服務(wù)的知識
- AsycTask 的使用
- 6.0 以上權(quán)限申請 EasyPermissions 的使用弧哎。
以上不熟悉的同學(xué),看到下邊的代碼可能會引起適量身體不適
其實這里更好的操作稚虎,我們可以將圖片下載到內(nèi)存中撤嫩,這樣并不需要申請sdk權(quán)限。這里當(dāng)時實現(xiàn)的時候有點欠考慮了蠢终。如果您們保存圖片的地址在內(nèi)存中序攘,就可以跳過這一步。
1. 權(quán)限管理
首先我們注意到已進入 Splash 頁面我們就進行權(quán)限檢查寻拂,因為我們需要下載最新的閃屏到本地程奠,并取出序列化的對象,來展示對應(yīng)的內(nèi)容祭钉。
其中 checkSDCardPermission
涉及到 6.0 以上下載最新圖片的邏輯瞄沙,這里采用的是 官方的 EasyPermissions 來處理,關(guān)于 EasyPermissions 的使用這里就不多說了慌核,需要了解的請移步 EasyPermissions距境;
public static final int RC_PERMISSION = 123;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@AfterPermissionGranted(RC_PERMISSION)
private void checkSDCardPermission() {
if (EasyPermissions.hasPermissions(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) {
initSplashImage();
startImageDownLoad();
} else {
EasyPermissions.requestPermissions(this, "需要您提供【**】App 讀寫內(nèi)存卡權(quán)限來確保應(yīng)用更好的運行", RC_PERMISSION, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
}
簡單來說在 EasyPermissions.hasPermissions
的回調(diào)中我們就可以正確的做我們下載圖片的工作了。
private void initSplashImage() {
mSplash = getLocalSplash();
//如果取出本地序列化的對象成功 則進行圖片加載和倒計時
if (mSplash != null && !TextUtils.isEmpty(mSplash.savePath)) {
Logcat.d("SplashActivity 獲取本地序列化成功" + mSplash);
Glide.with(this).load(mSplash.savePath).dontAnimate().into(mSpBgImage);
startClock();//加載成功 開啟倒計時
} else {
// 如果本地沒有 直接跳轉(zhuǎn)
mSpJumpBtn.setVisibility(View.INVISIBLE);
mSpJumpBtn.postDelayed(new Runnable() {
@Override
public void run() {
gotoLoginOrMainActivity();
}
}, 400);
}
}
// 取出本地序列化的 Splash
private Splash getLocalSplash() {
Splash splash = null;
try {
File serializableFile = SerializableUtils.getSerializableFile(Constants.SPLASH_PATH, Constants.SPLASH_FILE_NAME);
splash = (Splash) SerializableUtils.readObject(serializableFile);
} catch (IOException e) {
Logcat.e("SplashActivity 獲取本地序列化閃屏失敗" + e.getMessage());
}
return splash;
}
2. 創(chuàng)建本地序列化對象 Splash Entity
Splash 內(nèi)容如下:
public class Splash implements Serializable {
private static final long serialVersionUID = 7382351359868556980L;//這里需要寫死 序列化Id
public int id;
public String burl;//大圖 url
public String surl;//小圖url
public int type;//圖片類型 Android 1 IOS 2
public String click_url; // 點擊跳轉(zhuǎn) URl
public String savePath;//圖片的存儲地址
public String title;//圖片的存儲地址
public Splash(String burl, String surl, String click_url, String savePath) {
this.burl = burl;
this.surl = surl;
this.click_url = click_url;
this.savePath = savePath;
}
@Override
public String toString() {
return "Splash{" +
"id=" + id +
", burl='" + burl + '\'' +
", surl='" + surl + '\'' +
", type=" + type +
", click_url='" + click_url + '\'' +
", savePath='" + savePath + '\'' +
'}';
}
}
3. 序列化反序列話的工具類 SerializableUtils
由于項目用到序列化地方還有挺多的垮卓,所以這里封裝了一個序列化工具類SerializableUtils
:
public class SerializableUtils {
public static <T extends Serializable> Object readObject(File file) {
ObjectInputStream in = null;
T t = null;
try {
in = new ObjectInputStream(new FileInputStream(file));
t = (T) in.readObject();
} catch (EOFException e) {
// ... this is fine
} catch (IOException e) {
Logcat.e("e " + e.getMessage());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (in != null) in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return t;
}
public static <T extends Serializable> boolean writeObject(T t, String fileName) {
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream(fileName));
out.writeObject(t);
Logcat.d("序列化成功 " + t.toString());
return true;
} catch (IOException e) {
e.printStackTrace();
Logcat.d("序列化失敗 " + e.getMessage());
return false;
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static File getSerializableFile(String rootPath, String fileName) throws IOException {
File file = new File(rootPath);
if (!file.exists()) file.mkdirs();
File serializable = new File(file, fileName);
if (!serializable.exists()) serializable.createNewFile();
return serializable;
}
}
經(jīng)過上邊的努力我們已經(jīng)完成了從本地反序列化內(nèi)容垫桂,然后加載圖片的工作了,剩下的需要做的就是下載最新圖片的工作粟按。
4. 請求接口下載最新的閃屏信息和圖片
這里經(jīng)過考慮诬滩,我決定采用服務(wù)去下載,因為這樣可以少很多麻煩灭将,也不影響程序的正常運行疼鸟。但是絕不是你們要采用這樣的方法,你們也可以單獨寫個工具類內(nèi)部去開線程做這件事宗侦。
項目中使用開啟
IntentServie
來下載圖片愚臀,關(guān)于這中服務(wù)的最大的好處就是,我們不需要關(guān)注服務(wù)是否執(zhí)行完任務(wù)矾利,當(dāng)他執(zhí)行完 onHandleIntent 方法后他就自己挑用 stop 方法了。我們只需要關(guān)注下載邏輯和序列化邏輯就好馋袜。
checkSDCardPermission
中調(diào)用的 startImageDownLoad()
方法:
private void startImageDownLoad() {
SplashDownLoadService.startDownLoadSplashImage(this, Constants.DOWNLOAD_SPLASH);
}
SplashDownLoadService 內(nèi)容男旗,IntentService 在調(diào)用了 startService 后會執(zhí)行 onHandleIntent
方法,在這方法中我們?nèi)フ埱蠓?wù)器最新的數(shù)據(jù)即 loadSplashNetDate
:
public SplashDownLoadService() {
super("SplashDownLoad");
}
public static void startDownLoadSplashImage(Context context, String action) {
Intent intent = new Intent(context, SplashDownLoadService.class);
intent.putExtra(Constants.EXTRA_DOWNLOAD, action);
context.startService(intent);
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
if (intent != null) {
String action = intent.getStringExtra(Constants.EXTRA_DOWNLOAD);
if (action.equals(Constants.DOWNLOAD_SPLASH)) {
loadSplashNetDate();
}
}
}
由于是公司項目欣鳖,請求方法就不給出了察皇,但是需要講下請求數(shù)據(jù)后如何判斷是否需要執(zhí)行下載任務(wù):
mScreen = common.attachment.flashScreen;
Splash splashLocal = getSplashLocal();
if (mScreen != null) {
if (splashLocal == null) {
Logcat.d("splashLocal 為空導(dǎo)致下載");
startDownLoadSplash(Constants.SPLASH_PATH, mScreen.burl);
} else if (isNeedDownLoad(splashLocal.savePath, mScreen.burl)) {
Logcat.d("isNeedDownLoad 導(dǎo)致下載");
startDownLoadSplash(Constants.SPLASH_PATH, mScreen.burl);
}
} else {//由于活動是一段時間,等活動結(jié)束后我們并不需要在進入閃屏頁面,這個時候我們就需要將本地文件刪除什荣,下次在進來矾缓,本地文件為空,就會直接 finish 掉 Splash 頁面稻爬,進入主頁面嗜闻。
if (splashLocal != null) {
File splashFile = SerializableUtils.getSerializableFile(Constants.SPLASH_PATH, SPLASH_FILE_NAME);
if (splashFile.exists()) {
splashFile.delete();
Logcat.d("mScreen為空刪除本地文件");
}
}
}
由于活動是一段時間,等活動結(jié)束后我們并不需要在進入閃屏頁面桅锄,這個時候我們就需要將本地文件刪除琉雳,下次在進來,本地文件為空友瘤,就會直接 finish 掉 Splash 頁面翠肘,進入主頁面。
getSplashLocal
方法即反序列話本地存儲的 Splash Entity 的過程辫秧,上邊已經(jīng)給出這里就不細說束倍,主要講一下判斷邏輯 isNeedDownLoad
:
/**
* @param path 本地存儲的圖片絕對路徑
* @param url 網(wǎng)絡(luò)獲取url
* @return 比較儲存的 圖片名稱的哈希值與 網(wǎng)絡(luò)獲取的哈希值是否相同
*/
private boolean isNeedDownLoad(String path, String url) {
// 如果本地存儲的內(nèi)容為空則進行下載
if (TextUtils.isEmpty(path)) {
return true;
}
// 如果本地文件不存在則進行下載,這里主要防止用戶誤刪操作
File file = new File(path);
if (!file.exists()) {
return true;
}
// 如果兩者都存在則判斷圖片名稱的 hashCode 是否相同盟戏,不相同則下載
if (getImageName(path).hashCode() != getImageName(url).hashCode()) {
return true;
}
return false;
}
分隔 uri 取圖片名稱的方法:
private String getImageName(String url) {
if (TextUtils.isEmpty(url)) {
return "";
}
String[] split = url.split("/");
String nameWith_ = split[split.length - 1];
String[] split1 = nameWith_.split("\\.");
return split1[0];
}
滿足下載條件后則調(diào)用 DownLoadTask 下載绪妹。
public class DownLoadUtils {
public interface DownLoadInterFace {
void afterDownLoad(ArrayList<String> savePaths);
}
public static void downLoad(String savePath, DownLoadInterFace downLoadInterFace, String... download) {
new DownLoadTask(savePath, downLoadInterFace).execute(download);
}
private static class DownLoadTask extends AsyncTask<String, Integer, ArrayList<String>> {
private String mSavePath;
private DownLoadInterFace mDownLoadInterFace;
private DownLoadTask(String savePath, DownLoadInterFace downLoadTask) {
this.mSavePath = savePath;
this.mDownLoadInterFace = downLoadTask;
}
@Override
protected ArrayList<String> doInBackground(String... params) {
ArrayList<String> names = new ArrayList<>();
for (String url : params) {
if (!TextUtils.isEmpty(url)) {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// 獲得存儲卡的路徑
FileOutputStream fos = null;
InputStream is = null;
try {
URL downUrl = new URL(url);
// 創(chuàng)建連接
HttpURLConnection conn = (HttpURLConnection) downUrl.openConnection();
conn.connect();
// 創(chuàng)建輸入流
is = conn.getInputStream();
File file = new File(mSavePath);
// 判斷文件目錄是否存在
if (!file.exists()) {
file.mkdirs();
}
String[] split = url.split("/");
String fileName = split[split.length - 1];
File mApkFile = new File(mSavePath, fileName);
names.add(mApkFile.getAbsolutePath());
fos = new FileOutputStream(mApkFile, false);
int count = 0;
// 緩存
byte buf[] = new byte[1024];
while (true) {
int read = is.read(buf);
if (read == -1) {
break;
}
fos.write(buf, 0, read);
count += read;
publishProgress(count);
}
fos.flush();
} catch (Exception e) {
Logcat.e(e.getMessage());
} finally {
try {
if (is != null) {
is.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
return names;
}
@Override
protected void onPostExecute(ArrayList<String> strings) {
super.onPostExecute(strings);
if (mDownLoadInterFace != null) {
mDownLoadInterFace.afterDownLoad(strings);
}
}
}
}
由于下載完成后需要拿到文件存儲地址這里寫了一個 mDownLoadInterFace.afterDownLoad 的回調(diào)在 service 拿到回調(diào)后:
public void afterDownLoad(ArrayList<String> savePaths) {
if (savePaths.size() == 1) {
Logcat.d("閃屏頁面下載完成" + savePaths);
if (mScreen != null) {
mScreen.savePath = savePaths.get(0);
}
// 序列化 Splash 到本地
SerializableUtils.writeObject(mScreen, Constants.SPLASH_PATH + "/" + SPLASH_FILE_NAME);
} else {
Logcat.d("閃屏頁面下載失敗" + savePaths);
}
}
寫在最后
上邊 bb 這么多,我們可以看出產(chǎn)品一句話抓半,我們程序員可能就需要工作一天了喂急,所以我們需要將這個常見的功能記錄下,下個公司產(chǎn)品再說實現(xiàn)一個閃屏功能笛求,然后我們就可以說 這功能可能需要 1天時間廊移,然后等他答應(yīng)了,copy 一下探入,其他的時間你就可以學(xué)習(xí)下 Rxjava2 狡孔,kotlin, js 之類的了蜂嗽。哈哈哈哈 我真tm機智苗膝。
后記:
這篇文章投稿到掘金和鴻洋大神的公眾號后,大家對我的代碼提出了許多建議植旧,我感謝大家能幫助我成長辱揭。大家普遍要求一個Demo,花了幾個小時時間病附,將其從項目中抽出來问窃。希望大家賞臉 star 或者fork:
項目地址:SplashActivityDemo