经常使用华为手机的朋友一定有用到过华为系统,长按右下角菜单键,如果内存可以清除,就会出现一个上拉清除内存的功能界面。之前博客里也提到了,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的一些用法,收获真的很多,搞技术还是需要多看多了解啊,见多识广,遇到问题才能分析解决。
博客到这里也算告一段落了吧,有问题可以留言,希望能和大家多多交流~
版权声明:本文为博主原创文章,未经博主允许不得转载。