教你如何實現(xiàn) Splash 頁面三秒跳轉和動態(tài)下載最新背景圖

本文已授權公眾號hongyangAndroid原創(chuàng)首發(fā)

最近公司產品大大說我們需要一個動態(tài)替換的閃屏頁面,like 某貓,某東一樣,可以動態(tài)替換。
產品大大就是厲害,說一句話我們就需要實現(xiàn)好幾個功能:

  1. 創(chuàng)建一個冷啟動后的閃屏頁面(Splash 頁面)
  1. 這個頁面默認 3s 倒計時,點擊倒計時按鈕可以跳轉并結束倒計時
  1. 點擊圖片如果有外鏈,則跳轉應用的 web 頁面用來作為活動頁面(沒錯這點和某貓很像)
  1. 動態(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 的類,此類神奇之處就在于你完全不需要理會那些線程交互他都給你處理好了,你只管在回調中處理時間設置跳轉邏輯就好了。

但是有一個不足的地方就它的第一秒的倒計時有時候會不可見,所以我們將倒計時總時間設置為 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)點擊跳轉功能實現(xiàn)

上邊說了我們 APP 點擊圖片需要可以跳轉,下面代碼給出了背景點擊跳轉的邏輯:

    @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;
        }
    }

跳轉邏輯可以根據(jù)實際的項目需求來規(guī)定,下面的代碼中 Splash 為本地序列化的 model 用來存儲網絡下載的閃屏頁面信息,稍后會有詳細的序列化過程,此刻我們只需要關注跳轉邏輯:

   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();
        }
    }
    

機智的你可能看出來我們并沒有在離開頁面的時候結束掉 timer,其實我們是復寫了 onDestroy 方法。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (countDownTimer != null)
            countDownTimer.cancel();
    }

其實跳轉以后還有一個坑就是,從 web 頁面返回的時候,因為閃屏頁面是你應用的第一個頁面,而跳轉到 web 頁面的是你 finish 掉了該頁面,那么從 web 頁返回的時候不做處理,用戶就直接退出了 app 這樣當然是不允許的。

所以請在 web 頁面中添加以下邏輯:

    //此方法是toolbar 的返回事件調用的方法 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();
        }
    }
     // 下面是跳轉邏輯 
     private void gotoLoginOrMainActivity() {
        if (UserCenter.getInstance().getToken() == null) {
            gotoLoginActivity();
        } else {
            gotoMainActivity();
        }
    }

    .... gotoLoginActivity,gotoMainActivity 太長了,不給了自己寫 (*^__^*) 嘻嘻…… 

四、下載網絡圖片以及序列化本地

上邊說了我們有這樣一個需求,就是如果后臺的接口返回的圖片與本地序列化的圖片不同,我們需要將新的圖片下載到本地,然后下次進入 Splash 的時候就展示的新的圖片了。

這里你需要知道知識有下邊幾個:

  1. java bean 序列化與反序列化的知識
  2. IntentService 服務的知識
  3. AsycTask 的使用
  4. 6.0 以上權限申請 EasyPermissions 的使用。

以上不熟悉的同學,看到下邊的代碼可能會引起適量身體不適


其實這里更好的操作,我們可以將圖片下載到內存中,這樣并不需要申請sdk權限。這里當時實現(xiàn)的時候有點欠考慮了。如果您們保存圖片的地址在內存中,就可以跳過這一步。

1. 權限管理

首先我們注意到已進入 Splash 頁面我們就進行權限檢查,因為我們需要下載最新的閃屏到本地,并取出序列化的對象,來展示對應的內容。

其中 checkSDCardPermission 涉及到 6.0 以上下載最新圖片的邏輯,這里采用的是 官方的 EasyPermissions 來處理,關于 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 讀寫內存卡權限來確保應用更好的運行", RC_PERMISSION, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }
    }

簡單來說在 EasyPermissions.hasPermissions 的回調中我們就可以正確的做我們下載圖片的工作了。

    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 {
        // 如果本地沒有 直接跳轉
            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 內容如下:

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; // 點擊跳轉 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;
    }
}

經過上邊的努力我們已經完成了從本地反序列化內容,然后加載圖片的工作了,剩下的需要做的就是下載最新圖片的工作。

4. 請求接口下載最新的閃屏信息和圖片

這里經過考慮,我決定采用服務去下載,因為這樣可以少很多麻煩,也不影響程序的正常運行。但是絕不是你們要采用這樣的方法,你們也可以單獨寫個工具類內部去開線程做這件事。

項目中使用開啟 IntentServie 來下載圖片,關于這中服務的最大的好處就是,我們不需要關注服務是否執(zhí)行完任務,當他執(zhí)行完 onHandleIntent 方法后他就自己挑用 stop 方法了。我們只需要關注下載邏輯和序列化邏輯就好。

checkSDCardPermission 中調用的 startImageDownLoad() 方法:

 private void startImageDownLoad() {
    SplashDownLoadService.startDownLoadSplashImage(this, Constants.DOWNLOAD_SPLASH);
 }

SplashDownLoadService 內容,IntentService 在調用了 startService 后會執(zhí)行 onHandleIntent 方法,在這方法中我們去請求服務器最新的數(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í)行下載任務:

   mScreen = common.attachment.flashScreen;
           Splash splashLocal = getSplashLocal();
           if (mScreen != null) {
               if (splashLocal == null) {
                  Logcat.d("splashLocal 為空導致下載");
                  startDownLoadSplash(Constants.SPLASH_PATH, mScreen.burl);
                } else if (isNeedDownLoad(splashLocal.savePath, mScreen.burl)) {
                      Logcat.d("isNeedDownLoad 導致下載");
                      startDownLoadSplash(Constants.SPLASH_PATH, mScreen.burl);
               }
           } else {//由于活動是一段時間,等活動結束后我們并不需要在進入閃屏頁面,這個時候我們就需要將本地文件刪除,下次在進來,本地文件為空,就會直接 finish 掉 Splash 頁面,進入主頁面。
              if (splashLocal != null) {
                    File splashFile = SerializableUtils.getSerializableFile(Constants.SPLASH_PATH, SPLASH_FILE_NAME);
                     if (splashFile.exists()) {
                             splashFile.delete();
                             Logcat.d("mScreen為空刪除本地文件");
                       }
                }
           }

由于活動是一段時間,等活動結束后我們并不需要在進入閃屏頁面,這個時候我們就需要將本地文件刪除,下次在進來,本地文件為空,就會直接 finish 掉 Splash 頁面,進入主頁面。

getSplashLocal 方法即反序列話本地存儲的 Splash Entity 的過程,上邊已經給出這里就不細說,主要講一下判斷邏輯 isNeedDownLoad

    /**
     * @param path 本地存儲的圖片絕對路徑
     * @param url  網絡獲取url
     * @return 比較儲存的 圖片名稱的哈希值與 網絡獲取的哈希值是否相同
     */
    private boolean isNeedDownLoad(String path, String url) {
        // 如果本地存儲的內容為空則進行下載
        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];
    }

滿足下載條件后則調用 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 的回調在 service 拿到回調后:

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 這么多,我們可以看出產品一句話,我們程序員可能就需要工作一天了,所以我們需要將這個常見的功能記錄下,下個公司產品再說實現(xiàn)一個閃屏功能,然后我們就可以說 這功能可能需要 1天時間,然后等他答應了,copy 一下,其他的時間你就可以學習下 Rxjava2 ,kotlin, js 之類的了。哈哈哈哈 我真tm機智。

后記:

這篇文章投稿到掘金和鴻洋大神的公眾號后,大家對我的代碼提出了許多建議,我感謝大家能幫助我成長。大家普遍要求一個Demo,花了幾個小時時間,將其從項目中抽出來。希望大家賞臉 star 或者fork:

項目地址:SplashActivityDemo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容