一个叫GUN的有趣的APP源码

      这个APP是帮一个小伙伴开发的,功能和UI都超级简单,代码量很少,目前算是alpha版本吧,因为是开发着玩的(非公司项目),所以把目前的代码放送出来。

      这是在开发过程中注意的几个知识点:

  • 使用Material Design中的Ripple Effect;
  • 使用自定义进度条,中间显示倒计时时间,外圆白色逐渐减少,黑圈逐渐增加(两个Paint,一个绘制未完成的白圈,一个绘制完成的黑圈)
  • 在Service中开启TimerTask每隔100ms监听一次当前使用的APP是否为本APP,否则自动跳转到指定界面
  • 注意监听Activity设置 android:launchMode="singleTask"
  • 使用android:excludeFromRecents="true"把最近使用的应用程序的列表中排出此APP

      这是源代码目录:


      先看一下APP设计原型(有木有很渣渣的赶脚),就是按照这个设计敲的代码…… 

这是一个比较有恶趣味的 app
产品名称:「gun」,「鲧」的拼音,鲧是禹的爹,《大禹治水》的典故里有讲,鲧治水靠「堵」,禹治水靠「疏导」。不玩手机这事儿,大家都知道影响不好,道理都懂,但还是停不下来,能做到的毕竟是少数。除了靠「自律」,还可以用「堵」这个方式,直接不让用
功能定位:定时番茄钟
面向对象:手机依赖症,离不开手机,有强迫症的人,随时都要看看玩玩手机
产品特色:设定后不能中途暂停或结束,得等到设定的时间到了自动解禁,或者关机重启解禁,以更强迫的方式治疗强迫症
产品劣势:目前就我们俩,一个产品汪+一个攻城狮的配置,没有UI视觉设计,所以界面采用极简风格,黑白配
必备功能:彻底做到控制手机,不给用户机会暂停。原生的电话功能可使用
魅族和nexus5可通过删除曾经打开过的应用记录,退出应用,看下有没有办法能绝对控制权限
后续迭代版本功能:1.设置白名单功能,根据反馈添加一些不能强制禁掉的应用,2.数据统计功能,统计连续使用次数和总的成功使用的次数,写代码时注意下
产品辅助:已经注册了一个微博帐号「一个叫gun的app」,主要用来收集使用反馈,当然,可能粉丝不多
字体字号:雅痞-繁(如果没有可以采用微软雅黑)
【开始】和倒计时数字字号72,提示文字和颜文字字号24,启动页字号60 和 24
【不用不能活】字号72,颜文字字号36

 

应用启动页                                        进入软件页面显示内容

 

点击【开始】圆圈区域颜色变浅           显示倒计时,圆圈区域颜色恢复

 

未解锁状态,操作应用内任意区域显示文字提示      倒计时完成,提示使用记录                                                  


未解锁状态操作其他app的提示

说明:

  1. 点中时中间圆圈颜色变淡,使用Material动效:触控涟漪-按压/释放,参考http://www.ui.cn/project.php?id=18846
  2. 随机选择30分钟,33分33秒,35分钟这三种锁定时间
    操作【开始】进入倒计时,从30:00(或33:33,35:00)开始倒计时计数,开始计数后,白色圆圈逐渐减少白色区域大小

    计时结束后,白色圆圈又恢复到整圆,并显示连续使用记录
  3. 计时正常结束后显示统计连续使用并未强制停止的次数,次数用画#正#统计
    如果是重新加载启动app,不显示统计结果
    连续使用次数1-5   (^0^)/ 已连续使用 正 次
    连续使用次数6-10 o(^^o)  已连续使用 正正 次
    连续使用次数11-15  ( ^)o(^ )已连续使用 正正正 次
    连续使用次数 16-20 ヽ(^。^)丿连续使用 正正正正 次
    连续使用次数>21  (^_^;) 可以卸载了
  4. 未解锁状态下,操作应用内任意区域显示提示,1s后消失
  5. 操作安装的其他app和系统自带应用(电话功能除外)均显示此界面
设计原型看完了,就开始Coding吧:
那些BaseActivity,AppManager之类的项目中常用的就不贴出来了,为了控制篇幅,只贴核心功能代码。
先看一下activity_main.xml,UI要注意的地方都在这里了
<com.wen.gun.ripplelibrary.RippleBackground xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    app:rb_color="@color/center_bg_color"
    app:rb_duration="1000"
    app:rb_radius="60dp"
    app:rb_rippleAmount="1"
    app:rb_scale="8" >

    <TextView
        android:id="@+id/tv_notes"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/tv_start"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="@dimen/mar_bo_main_note"
        android:textColor="@color/ivory"
        android:textSize="@dimen/text_main_note" />

    <TextView
        android:id="@+id/tv_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@drawable/bg_tv"
        android:gravity="center"
        android:lineSpacingMultiplier="1.2"
        android:text="@string/main_mid_start"
        android:textColor="@color/ivory"
        android:textSize="@dimen/text_splash_mid" />  
    <com.wen.gun.circleprogress.DonutProgress
        xmlns:custom="http://schemas.android.com/apk/res-auto"
        android:id="@+id/donut_progress"
        android:layout_width="@dimen/center_monitor_size"
        android:layout_height="@dimen/center_monitor_size"
        android:layout_centerInParent="true"
        android:background="@color/black"
        android:visibility="invisible"
        custom:donut_finished_color="@color/black"
        custom:donut_finished_stroke_width="10dp"
        custom:donut_inner_bottom_text=""
        custom:donut_progress="0"
        custom:donut_text_size="@dimen/text_monitor_time"
        custom:donut_unfinished_color="@color/ivory"
        custom:donut_unfinished_stroke_width="@dimen/stroke_size" />
</com.wen.gun.ripplelibrary.RippleBackground>
com.wen.gun.ripplelibrary.RippleBackground设置Ripple Effect
com.wen.gun.circleprogress.DonutProgress自定义进度条,外面是圆圈,中间是数字
		canvas.drawArc(finishedOuterRect, 360 - getProgressAngle(), 270, false,
				finishedPaint); // black
		canvas.drawArc(unfinishedOuterRect, 270, 360 - getProgressAngle(),
				false, unfinishedPaint); // white
在drawArc的时候,注意右边中间是0°,正上方是270°(-90°),我这里是逆时针,如果想要圆圈进度效果为顺时针,修改startAngle和sweepAngle就可以了:
		 canvas.drawArc(finishedOuterRect, -90,getProgressAngle(), false,
		 finishedPaint); //black
	     canvas.drawArc(unfinishedOuterRect, -90+getProgressAngle(),
		 360-getProgressAngle(), false, unfinishedPaint); //white
至于bg_tv.xml,是为文字设置一个oval的背景,加上边框,填充就可以
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval"
    android:useLevel="false" >

    <solid android:color="@color/center_bg_color" />

    <stroke
        android:width="@dimen/stroke_size"
        android:color="@color/ivory" />

    <size
        android:height="@dimen/center_start_size"
        android:width="@dimen/center_start_size" />

</shape>

再看看MainActivity
<pre name="code" class="java">public class MainActivity extends BaseActivity {
	private static String TAG = MainActivity.class.getName();
	private TextView tv_start, tv_notes;
	private RippleBackground rippleBackground;
	private DonutProgress donutProgress;
	private boolean isStart;
	private MyCount mc;
	private AlphaAnimation mHideAnimation = null;
	private AlphaAnimation mShowAnimation = null;
	private int animationTime;
	private String mDTime;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		initView();
		initData();
		setLinstener();
		fillData();
	}

	@Override
	protected void initData() {
		// int times = (Integer) SPUtils.get(MainActivity.this, "use_times", 0);
		tv_notes.setText(StringNotsUtils.setText(0));
		setMonitorDurationTime();
		L.i(TAG, "这次随机生成的时间为=" + Constant.TIME_DURATION);
		animationTime = 1000;
		mc = new MyCount(Constant.TIME_DURATION * 1000 + 1000, 1000); // 加上1秒
		donutProgress.setMax(Constant.TIME_DURATION);
		isStart = false;

	}

	@Override
	protected void initView() {
		tv_start = (TextView) this.findViewById(R.id.tv_start);
		tv_notes = (TextView) this.findViewById(R.id.tv_notes);
		rippleBackground = (RippleBackground) findViewById(R.id.content);
		donutProgress = (DonutProgress) this.findViewById(R.id.donut_progress);
	}

	@Override
	protected void setLinstener() {
		tv_start.setOnClickListener(this);
		donutProgress.setOnClickListener(this);

	}

	@Override
	protected void fillData() {
		// TODO Auto-generated method stub

	}

	@Override
	public void onClick(View v) {
		switch (v.getId()) {
		case R.id.tv_start:
			if (!isStart) {
				isStart = true;
				donutProgress.setProgress(0);
				setRippleEffect();
				stratService();
			} else {
				L.i(TAG, "已经开始了额");
			}

			break;

		case R.id.donut_progress:
			T.showLong(MainActivity.this, "你将在  " + mDTime + " 后恢复手机使用权");
			break;
		default:
			break;
		}

	}

	/**
	 * TODO<点击开始的时候出现涟漪效果,3s后停止涟漪效果,并开启倒计时>
	 * 
	 * @throw
	 * @return void
	 */
	private void setRippleEffect() {

		rippleBackground.startRippleAnimation();

		new Handler().postDelayed(new Runnable() {
			@Override
			public void run() {
				rippleBackground.stopRippleAnimation();
				tv_start.setVisibility(View.INVISIBLE);
				donutProgress.setVisibility(View.VISIBLE);
				donutProgress.setClickable(true);
				setHideAnimation(tv_start, animationTime);
				setShowAnimation(donutProgress, animationTime);
				Countdown();

			}
		}, 3000);

	}

	/**
	 * TODO<开启倒计时>
	 * 
	 * @throw
	 * @return void
	 */
	private void Countdown() {
		tv_notes.setText("再等等 或 关机重启");
		mc.start();// 开启倒计时
	}

	/**
	 * TODO<结束一次治疗后准备下一次的治疗数据准备>
	 * 
	 * @throw
	 * @return void
	 */
	public void prepareNewTask() {
		L.i(TAG, "这次结束了");
		isStart = false;
		tv_start.setVisibility(View.VISIBLE);
		donutProgress.setProgress(0);
		donutProgress.setVisibility(View.INVISIBLE);
		donutProgress.setClickable(false);
		setHideAnimation(donutProgress, animationTime);
		setShowAnimation(tv_start, animationTime);
		stopService();
		int times = (Integer) SPUtils.get(MainActivity.this, "use_times", 0);
		tv_notes.setText(StringNotsUtils.setText(times));
	}

	/**
	 * TODO<保存治疗次数>
	 * 
	 * @throw
	 * @return void
	 */
	public void saveUseTimes() {

		int times = (Integer) SPUtils.get(MainActivity.this, "use_times", 0);
		SPUtils.put(MainActivity.this, "use_times", times + 1);
	}

	/**
	 * TODO<设置开始界面与倒计时界面切换时的动画效果>
	 * 
	 * @param view
	 * @param duration
	 * @throw
	 * @return void
	 */
	private void setHideAnimation(View view, int duration) {

		if (null == view || duration < 0) {

			return;

		}

		if (null != mHideAnimation) {

			mHideAnimation.cancel();

		}

		mHideAnimation = new AlphaAnimation(1.0f, 0.0f);

		mHideAnimation.setDuration(duration);

		mHideAnimation.setFillAfter(true);

		view.startAnimation(mHideAnimation);

	}

	private void setShowAnimation(View view, int duration) {

		if (null == view || duration < 0) {

			return;

		}

		if (null != mShowAnimation) {

			mShowAnimation.cancel();

		}

		mShowAnimation = new AlphaAnimation(0.0f, 1.0f);

		mShowAnimation.setDuration(duration);

		mShowAnimation.setFillAfter(true);

		view.startAnimation(mShowAnimation);

	}

	/**
	 * TODO<开始监听服务>
	 * 
	 * @throw
	 * @return void
	 */
	private void stratService() {
		Intent intent = new Intent(MainActivity.this, MonitorService.class);
		startService(intent);

	}

	/**
	 * TODO<停止服务>
	 * 
	 * @throw
	 * @return void
	 */
	private void stopService() {
		Intent intent = new Intent(MainActivity.this, MonitorService.class);
		stopService(intent);
	}

	/* 定义一个倒计时的内部类 */
	class MyCount extends CountDownTimer {
		public MyCount(long millisInFuture, long countDownInterval) {
			super(millisInFuture, countDownInterval);
		}

		// 最后一次不会再调用
		@Override
		public void onTick(long millisUntilFinished) {
			L.i(TAG, "我在倒计时");
			if (donutProgress.getProgress() != Constant.TIME_DURATION) {
				donutProgress.setProgress(donutProgress.getProgress() + 1);
				mDTime = TimeConvert.secondsToMinute1(Constant.TIME_DURATION
						- donutProgress.getProgress());

			}

		}

		/**
		 * 重载方法,如果有需要可以在这里关闭APP
		 */
		@Override
		public void onFinish() {
			saveUseTimes();
			prepareNewTask();

			L.i(TAG, "倒计时结束");
			new Handler().postDelayed(new Runnable() {
				@Override
				public void run() {
					// AppManager.getInstance().AppExit(getApplicationContext());
				}
			}, 3000);

		}
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();

		// mc.cancel(); 保持运行
		if (mShowAnimation != null) {
			mShowAnimation.cancel();
		}
		if (mHideAnimation != null) {
			mHideAnimation.cancel();
		}

	}

	// 随机选择30分钟,33分33秒,35分钟这三种锁定时间
	public void setMonitorDurationTime() {
		switch (StringRandomUtils.getRandomNumber(1, 3)) {
		case 1:
			Constant.TIME_DURATION = 1800; // 30min
			// Constant.TIME_DURATION = 60;

			break;
		case 2:
			Constant.TIME_DURATION = 2013; // 33分33秒
			// Constant.TIME_DURATION = 60;

			break;
		case 3:
			Constant.TIME_DURATION = 2100; // 35min
			// Constant.TIME_DURATION = 60;

			break;

		default:
			break;
		}
	}
}
 
 
说明:
  • 在进入APP的时候,调用setMonitorDurationTime()随机选择30分钟,33分33秒,35分钟这三种锁定时间;
  • 点击开始的时候,setRippleEffect()产生Ripple Effect效果,并开启MonitorService,3s后开启倒计时(倒计时的方式太多种了,我这里用的是CountDownTimer),在onTick中每隔一秒更新一次UI(圆圈进度和中间的倒计时文字)。
  • 倒计时结束onFinish中停止MonitorService,并重置一些参数。
再来看看MonitorService:
public class MonitorService extends Service {

	boolean flag = true;// 用于停止线程
	private ActivityManager activityManager;
	private Timer timer;
	private TimerTask task = new TimerTask() {

		@Override
		public void run() {
			// TODO Auto-generated method stub
			if (activityManager == null) {
				activityManager = (ActivityManager) MonitorService.this
						.getSystemService(ACTIVITY_SERVICE);
			}

			List<RecentTaskInfo> recentTasks = activityManager.getRecentTasks(
					2, ActivityManager.RECENT_WITH_EXCLUDED);
			RecentTaskInfo recentInfo = recentTasks.get(0);
			Intent intent = recentInfo.baseIntent;
			String recentTaskName = intent.getComponent().getPackageName();
	
//			
//			||!recentTaskName.equals("com.android.contacts")
//			||!recentTaskName.equals("com.android.phone")
//			||!recentTaskName.equals("com.android.launcher")
//			||!recentTaskName.equals("com.miui.home")
			
				if (!recentTaskName.equals("com.wen.gun")
						) {
					L.i("MonitorService", "Yes--recentTaskName=" + recentTaskName);
					Intent intentNewActivity = new Intent(MonitorService.this,
							MonitorActivity.class);
					intentNewActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
					startActivity(intentNewActivity);

				}else{
					L.i("MonitorService", "No--recentTaskName="+recentTaskName);

				

				}
			}
	

	};

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		
		if (flag == true) {
			timer = new Timer();
			timer.schedule(task, 0, 100);		
			flag = false;
		}

		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onDestroy() {
		// TODO Auto-generated method stub
		super.onDestroy();
		timer.cancel();
	}

	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}
}

原理很简单,就是开启一个TimerTask,不断的轮循取出当前的recentInfo,如果不是本APP,就跳转到MonitorActivity.class,注意,这里要设置setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)。
还有要注意AndroidManifest.xml配置。
代码很少,思路很简单!
最后,加上友盟或者百度统计就可以了……

猜你喜欢

转载自blog.csdn.net/yalinfendou/article/details/45361553