前言
实现效果
源码地址
https://github.com/littlecurl/AppProjects
进去找AndroidCountDown或者AndroidCountDown.zip进行下载
前提条件
【红黑联盟】《Android性能优化之Splash页应该这样设计》 主要讲了启动页优化,要把Activity变成Fragment
【深夜网】《App启动页和引导页注意事项》 主要讲了心理学中的七秒理论
【CSDN】《Android CountDownTimer 倒计时器的简单使用》 注意将了避免内存泄漏
正文
本来想直接写出优化后的效果,也就是直接用Fragment来写,但是那样对于第一次接触的人来说,学习曲线过于陡峭,所以我还是花点时间写两份吧,先写一份Activity的,再写一封优化后的Fragment的。
CountDownActivity
制作倒计时页面流程
-
new一个新坑,在对应包下创建一个EmptyActivity,起名为CountDownActivity
-
修改AndroidManifest.xml文件中启动页为CountDownActivity
3. 设置布局activity_count_down.xml,主布局设置背景图片,这里我还是沿用上次的写bug图,布局内只放一个TextView控件即可,内容如下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bug"
tools:context=".CountDownActivity">
<!-- android:gravity="center" 使文字在background内居中 -->
<TextView
android:id="@+id/tv_count_down"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:background="@drawable/bg_circle_countdown"
android:gravity="center"
android:text="跳过"
android:textColor="#2c2c2c"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
注意TextView也有background,这个background是一个xml文件,放在了drawable目录下(话说自己动手用xml画一个圆形,我还是头一次见)
<?xml version="1.0" encoding="utf-8"?>
<!-- 定义一个半径为50dp的圆形 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#64ffffff" />
<size
android:width="50dp"
android:height="50dp" />
</shape>
-
布局设置完毕,就该写CountDownActivity了,内容如下
package cn.edu.heuet.androidcountdown;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
public class CountDownActivity extends AppCompatActivity {
private CountDownTimer timer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setFullScreen();
// 设置全屏一定要在setContentView()之前,否则有可能不起作用
setContentView(R.layout.activity_count_down);
TextView countDownText = findViewById(R.id.tv_count_down);
initCountDown(countDownText);
}
// 全屏显示
private void setFullScreen() {
// 如果该类是 extends Activity ,下面这句代码起作用
// 去除ActionBar(因使用的是NoActionBar的主题,故此句有无皆可)
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 如果该类是 extends AppCompatActivity, 下面这句代码起作用
if (getSupportActionBar() != null){ getSupportActionBar().hide(); }
// 去除状态栏,如 电量、Wifi信号等
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
// 倒计时逻辑
private void initCountDown(final TextView countDownText) {
// 判断当前Activity是否isFinishing(),
// 避免在finish,所有对象都为null的状态下执行CountDown造成内存泄漏
if (!isFinishing()) {
timer = new CountDownTimer(1000 * 6, 1000) {
@SuppressLint("SetTextI18n")
@Override
public void onTick(long millisUntilFinished) {
// TODO: 耗时操作,如异步登录
// ......
int time = (int) millisUntilFinished;
countDownText.setText(time / 1000 + " 跳过");
countDownText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkToJump();
}
});
}
@Override
public void onFinish() {
checkToJump();
}
}.start();
}
}
// 首次进入引导页判断
private void checkToJump() {
// TODO:首次安装判断
// 如果是首次安装打开,则跳至引导页;否则跳至主界面
// 这里先不放引导页,直接跳到主界面
startActivity(new Intent(CountDownActivity.this, MainActivity.class));
// 回收内存
destoryTimer();
finish();
}
public void destoryTimer() {
// 避免内存泄漏
if (timer != null) {
timer.cancel();
timer = null;
}
}
}
这样就能实现一个Android APP的倒计时页了,接下来对其进行优化。
CountDownFragment
一旦使用了Fragment,好像一切东西都变得复杂了。
但换个角度说,还是用的少,这些编码技术相关的东西,无他,熟能生巧耳!
所以我们紧要的事情就是去见识它们,接近它们,理解他们,运用他们。
接下来进入正题
我们先修改一下AndroidManifest.xml文件,省的一会忘了。
然后我们创建一个CountDownFragment 类,extends Fragment.
前几天我写代码还是继承自v4包里的Fragment,这几天用上了AndroidStudio3.5,结果就是不再继承v4包了,而是换成了androidx包,但不要慌张,仅仅是包名换了,用法一点没变。变化详情可以参考下面这篇文章:
【简书】《Android AndroidX的迁移》 主要介绍了Android控件库的迁移及需要做的改变
如果对Fragment声明周期(就是方法执行顺序)还不了解的,可以参考
【博客园】《Fragment 生命周期的详情》 主要将官网的一些英文翻译了一下,并且配有Fragment官方声明周期图
我们先总览一下CountDownFragment类中的内容,总共就5部分
具体代码如下
package cn.edu.heuet.androidcountdown;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.fragment.app.Fragment;
public class CountDownFragment extends Fragment {
private View rootView;
private CountDownTimer timer;
private ConstraintLayout cl_main_activity;
// 官方文档里说了,最好每个Fragment都有一个空的构造方法
public CountDownFragment(){
}
@Nullable
@Override
public View onCreateView(@Nullable LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState){
rootView = inflater.inflate(R.layout.fragment_count_down, container, false);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
TextView countDownText = rootView.findViewById(R.id.tv_count_down);
// 隐藏主布局,注意这里用getActivity()来获取布局
// rootView 指代的是 R.layout.fragment_count_down
// getActivity() 指代的是 R.layout.activity_main
cl_main_activity = getActivity().findViewById(R.id.cl_main_activity);
cl_main_activity.setVisibility(View.GONE);
initCountDown(countDownText);
}
// 倒计时逻辑
private void initCountDown(final TextView countDownText) {
// 判断当前Fragment是否加入到了Activity中
if (isAdded()) {
timer = new CountDownTimer(1000 * 6, 1000) {
@SuppressLint("SetTextI18n")
@Override
public void onTick(long millisUntilFinished) {
// TODO: 耗时操作,如异步登录
// ......
int time = (int) millisUntilFinished;
countDownText.setText(time / 1000 + " 跳过");
countDownText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkToJump();
}
});
}
@Override
public void onFinish() {
checkToJump();
}
}.start();
}
}
// 首次进入引导页判断
private void checkToJump() {
// TODO:首次安装判断
// 如果是首次安装打开,则跳至引导页;否则跳至主界面
// 回收内存
destoryTimer();
// 移除Fragment,相当于Activity中的finish()
if (isAdded() && getFragmentManager() != null) {
getFragmentManager().beginTransaction().remove(this).commit();
// 显示主布局
cl_main_activity.setVisibility(View.VISIBLE);
}
}
private void destoryTimer() {
// 避免内存泄漏
if (timer != null) {
timer.cancel();
timer = null;
}
}
}
CountDownFragment类写好之后,就是在MainActivity中让他显示出来。
为此,我们修改main_activity.xml布局如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:id="@+id/fl_count_down"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_main_activity"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_to_count_down_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点我去CountDownActivity"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</TextView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
可以看到,专门为Fragment开辟了一个FrameLayout布局,将其他布局一并放入嵌套的ConstraintLayout中作为主布局。
package cn.edu.heuet.androidcountdown;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
countDownFragment();
TextView tv_to_count_down_activity = findViewById(R.id.tv_to_count_down_activity);
tv_to_count_down_activity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, CountDownActivity.class));
}
});
}
private void countDownFragment(){
// 开启事务
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment currentFragment = new CountDownFragment();
// 没有被加入的话,先加入
if (!currentFragment.isAdded()) {
getSupportFragmentManager().beginTransaction().remove(currentFragment).commit();
ft.add(R.id.fl_count_down, currentFragment);
}
// 显示Fragment
ft.show(currentFragment);
// commit
ft.commitAllowingStateLoss();
}
}
MainActivity类方法内容不是很多,主要难理解的就是Fragment使用了事务的方式来进行添加,展示。还是那句话,无他,孰能生巧耳!
本文结束!