Android仿照华为长按功能键实现清除内存功能
经常使用华为手机的朋友一定有用到过华为系统,长按右下角菜单键,如果内存可以清除,就会出现一个上拉清除内存的功能界面。之前博客里也提到了,f一直想做出这个效果,琢磨了一段时间,基本做出了雏形,不过做的只是下拉,圆弧从没有到完整闭合的效果,没有融入属性动画(华为系统默认效果有个类似皮球落地反复弹跳的动画),f本身对于动画不感冒,所以没有写进去,如果有人感兴趣,可以在我的基础上添加,同时f也没把具体清除的内存写进去,因为是调用系统的一些简单功能,大家有兴趣可以自己查一下。f旨在学习一下自定义view,viewgroup。这里先膜拜鸿洋大神,他的自定义view我已经连续学习了好多天了,还会一直坚持下去。下面正文开始。
1.简单分析一下,手指必须是触碰最下边的布局,才能实现清除功能,所以我们打算给整个view或者是viewgroup设置onTouchListener,往上拉的时候,圆弧开始出现,当向上滑动的距离,等于圆的直径时,圆弧出现一半,等于两倍直径时,圆弧闭合成为一个完整的圆。超过两倍直径再上拉不会动,松手就清除了内存,如果未超过两倍直径,布局会回去,同时圆弧也消失。
2.第一步,我们先画圆弧。
public class MyViewOne extends View{
private int mLastY;
private int mScreenHeight,mScreenWidth;
private RectF rectF;
public static final int mRadius = 50;
private Paint paint;
private int startHeight = 50;
public float sweepAngle = 0;
private boolean clear;
private int mStrokenWidth = 2;
private int viewHeight;
public MyViewOne(Context context, AttributeSet attrs,float sweepAngle) {
super(context, attrs);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mScreenHeight = outMetrics.heightPixels;
mScreenWidth = outMetrics.widthPixels;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStrokenWidth);//设置画笔末端的宽度
paint.setStrokeCap(Paint.Cap.ROUND);//设置画笔末端是圆角
this.sweepAngle = sweepAngle;//圆弧的闭合角度
Log.i("abc", "sweepAngle:"+sweepAngle);
}
public MyViewOne(Context context) {
this(context, null,0f);
}
public MyViewOne(Context context,float sweepAngle){
this(context,null,sweepAngle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
rectF = new RectF(mScreenWidth/2 - mRadius,startHeight,mScreenWidth/2+mRadius,2*mRadius+startHeight);
canvas.drawArc(rectF, -90, sweepAngle, false, paint);
}
这里需要一些自定义view的基础,以及如何画圆弧。需要提醒一下,drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)画圆弧时候,startAngle是圆弧开始的角度,以三点钟为0,六点钟是90,九点钟是180,12点钟是-90,顺时针画弧,sweepAngle是圆弧的角度,也就是开始于结束的角度差,注意是差值,超过360就是圆。
3.分析华为的系统功能,有两段文本提示内存的相关消息,所以我画了一个布局,准备在自定义viewgroup时候,add上去。
public class MyViewGroup extends LinearLayout{
MyViewOne view;
private Context context;
private TextView tvMemory,tvPullToClear;
private UpdateMemoryListener updateMemoryListener;
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(final Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
View child = LayoutInflater.from(context).inflate(R.layout.progress , null);
viewHeight = child.getMeasuredHeight();
tvMemory = (TextView) child.findViewById(R.id.tv_memory);
tvPullToClear = (TextView) child.findViewById(R.id.tv_pull_to_clear);
addView(child);
}
public void update(float sweepAngle , int dy ){
if(null != getChildAt(1)){
removeViewAt(1);
}//每次加之前,需要把上一个删掉。由于整体是动态add上去的,第一个add的child,下标为0,后面add的MyViewOne下标为1
//if(updateMemoryListener != null){}需要重写的方法
view = new MyViewOne(context,null,sweepAngle);
LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
lp.topMargin = 30;
view.setLayoutParams(lp);
addView(view);
scrollTo(0, -dy);//后面做解析
}
public interface UpdateMemoryListener{
public String updateAvailableMemory();
public String getTotalMemory();
}
public void setUpdateMemoryListener(UpdateMemoryListener updateMemoryListener){
this.updateMemoryListener = updateMemoryListener;
}
UpdateMemoryListener这个接口主要是提供内存信息的接口,可以在activity里,给MyViewGroup设置UpdateMemoryListener监听,重写方法,同时要修改一下update方法。我没有用,所以注掉了。
继承自线性布局,所以需要我们指定布局的排列方法。如果是垂直排列,那么布局就是往下面加,所以后面addview就是加到下面的。
下面贴progress的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_height="match_parent">
<TextView
android:layout_marginTop="10dip"
android:id="@+id/tv_memory"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_below="@+id/tv_memory"
android:id="@+id/tv_pull_to_clear"
android:layout_marginTop="10dip"
android:gravity="center_horizontal"
android:text="向下滑动,清除全部应用"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
4.然后怎么用呢?就是写到布局文件,加载到activity中。下面是布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#e0000000"
android:id="@+id/rl"
>
<com.fjf.pulltoclear.MyViewGroup
android:orientation="vertical"
android:id="@+id/my_view"
android:layout_width="match_parent"
android:layout_height="match_parent"></com.fjf.pulltoclear.MyViewGroup>
</RelativeLayout>
下面是activity
public class MainActivity extends Activity {
private MyViewGroup view;
private int mLastY;
private RelativeLayout rl;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_pulltoclear);
initViews();
}
private void initViews(){
rl = (RelativeLayout) findViewById(R.id.rl);
rl.getBackground().setAlpha(100);
view = (MyViewGroup) findViewById(R.id.my_view);
view.setUpdateMemoryListener(new UpdateMemoryListener() {
@Override
public String updateAvailableMemory() {
// TODO Auto-generated method stub
return null;
}
@Override
public String getTotalMemory() {
// TODO Auto-generated method stub
return null;
}
});
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
int y = (int) event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
return true;
case MotionEvent.ACTION_MOVE:
int dy = y - mLastY;//获得move的距离
Log.i("abc", "dy:" + dy);
//向下拉
if (dy > 0) {
//如果下拉的距离未超过两倍直径,即四倍半径
if (dy < 4 * MyViewOne.mRadius) {
view.update(1.8f * dy, dy);
} else {
//超过了,就显示一个完整的圆
view.update(360f, 4 * MyViewOne.mRadius);
}
//如果往上拉,则不动
} else if (dy <= 0) {
view.update(0f, 0);
}
return true;
case MotionEvent.ACTION_UP:
//同样判断是下拉
if (y - mLastY > 0) {
//如果未超过四倍半径,不清理内存,同时布局回到原位,圆弧要消失
if (y - mLastY < 4 * MyViewOne.mRadius) {
Log.i("abc", "不清除内存");
view.update(0f, 0);
//否则清除内存,关闭当前界面
} else {
Log.i("abc", "清除内存");
Toast.makeText(MainActivity.this, "内存已经释放", Toast.LENGTH_SHORT).show();
finish();
}
}
return true;
}
return false;
}
});
}
}
注意如果要提醒内存,注意UpdateMemoryListener的实现
touch事件的注解也比较详细,现在解析一下MyViewGroup的update()方法,重点说明滑动和闭合圆弧的角度。
华为系统的实现,滑动距离为直径时候,显示一半圆,滑动距离为两倍直径,显示完整的圆。两种情况,直径与圆弧的比例分别为R:180,2R:360,所以距离:弧度=R:180,我们定了圆的半径为50,所以弧度=180*距离/R,也就是弧度=1.8×距离。至于scroll,更简单了,只调用最基本的api即可,因为是向下滑动,所以scroll传入的值为负即向下滑动,而x轴保持不变,所以调用scrollTo( 0 , -距离);即可,注意,我们向下滑才出发update方法,下滑的距离为正。
基本的解析就是这些,接下来要分享一下f完成这个功能时候遇到的一个问题。
5.问题总结
f最开始,把事件分发写到了MyViewOne中,而progress布局中的textview是写到mainactivity的布局中,这样的结果是整个界面在滑动的过程中一直抖动,一直抖,当初是用postInvalidate()方法实现重绘的,f分析认为,可能是由于这些子控件不是一个view或者viewgroup,如果把他们封装到一起,可能就不会抖动。当然现在这种方法和每次根据传入的角度new一个view添加上,我认为这种处理,对于一直抖动的情况可能有效。
而且比较2的是,继承自线性布局,我却没有指定方法,圆弧一直出不来,快郁闷死了,后来去看线性布局的源码才意识到没有指定方向啊。。。
f研究这个控件有一段时间,最开始脑袋有点乱,等f把单独的功能分开以后,有种豁然开朗的感觉,也算是经验吧,对于复杂的内容,分割开,一块块解决,可能更顺利一些。
f也深入学习了鸿洋的自定义view,深入去理解scroller的一些用法,收获真的很多,搞技术还是需要多看多了解啊,见多识广,遇到问题才能分析解决。
博客到这里也算告一段落了吧,有问题可以留言,希望能和大家多多交流~
版权声明:本文为博主原创文章,未经博主允许不得转载。