MobileSafe Day06

Day06##

6.1复习#

前天讲完之后最后总结了,可回看下

6.2 拨打电话时显示号码归属地 # (重点,复习代码注册广播接收者)

来电界面有显示归属地,拨打电话界面也应有归属地



在外拨电话界面显示归属地,要去监听外拨电话事件,监听外拨电话事件一般都是通过广播实现

逻辑:设置中心如打开显示号码归属地,则外拨电话时显示号码归属地,否则外拨电话时关闭显示号码归属地



广播接收者清单文件注册和代码注册区别:



1)清单文件注册

	在清单文件中注册的广播接收者,程序一安装广播接收者就会一直有效,无法控制广播开始和结束

2)代码注册广播接收者

	注册广播接收者代码所在的程序运行,广播接收者才会生效,杀死进程,广播接收者就会失效,同时可以去注销广播接收者

	 可以控制广播接收者打开关闭



所以要用代码注册广播接收者去监听外拨电话,在onReceive中实现显示号码归属地功能



1.AddressService的onCreate中代码注册外拨电话的广播接收者

  	来清单文件中拿短信的广播接收者为例,看下注册广播接收者需要哪些东西:



		<receiver android:name="cn.itcast.mobilesafexian02.receiver.SmsReceiver" >

            <intent-filter android:priority="1000" >

                <action android:name="android.provider.Telephony.SMS_RECEIVED" />

            </intent-filter>

        </receiver>



	需要广播接收者名称name,广播过滤条件intent-filter,action表明要去接收哪个广播事件



	在AddressService中,创建外拨电话的广播接收者MyOutGoingCall

	把它在AddressService的onCreate中new出来

	new一个广播过滤条件IntentFilter,通过addAction设置要接收的广播事件

	用registerReceiver(receiver,filter)注册广播接收者

	有注册就有注销,在AddressService的onDestroy中当服务退出时去注销广播接受者	



		/**
  • 外拨电话的广播接受者
     */
     public class MyOutGoingCall extends BroadcastReceiver{
     @Override
     public void onReceive(Context context, Intent intent) {

      		}
    
      	}
    
      	@Override
    
      	public void onCreate() {
    
      		super.onCreate();
    
      		addressDao = new AddressDao();
    
      	
    
      		// 注册外拨电话的广播接受者
    
      		// 1.广播接受者
    
      		myOutGoingCall = new MyOutGoingCall();
    
      		// 2.广播的过滤条件
    
      		IntentFilter intentFilter = new IntentFilter();
    
      		intentFilter.addAction("android.intent.action.NEW_OUTGOING_CALL");// 设置广播接受者要接受的广播事件
    
      		// 3.注册广播接受者
    
      		registerReceiver(myOutGoingCall, intentFilter);
    
      	}
    
      	@Override
    
      		public void onDestroy() {
    
      			super.onDestroy();
    
    
    
      			// 注销广播接受者
    
      			unregisterReceiver(myOutGoingCall);
    
      		}
    
    
    
      	这就是代码注册注销外拨电话的广播接收者
    

    2.到onReceive中实现显示号码归属地的功能

      2.1 onReceive中获取外拨电话的号码
    
      	getResultData()可以获取广播接收者执行操作的数据
    
      	外拨电话执行的就是拨打电话,执行操作的数据就是外拨电话号码,返回String类型外拨电话号码resultData
    
    
    
      	public class MyOutGoingCall extends BroadcastReceiver {
    
      		@Override
    
      		public void onReceive(Context context, Intent intent) {
    
      			// 获取外拨电话的号码,获取广播接受者执行操作的数据
    
      			String resultData = getResultData();
    
      		}
    
      	}
    
      2.2根据拿到的号码查询号码归属地
    
      	queryAddress(num,context),参数num就是要查询归属地的电话号码:
    
    
    
      	public class MyOutGoingCall extends BroadcastReceiver {
    
      		@Override
    
      		public void onReceive(Context context, Intent intent) {
    
      			// 获取外拨电话的号码,获取广播接受者执行操作的数据
    
      			String resultData = getResultData();
    
      			// 查询号码归属地
    
      			String queryAddress = addressDao.queryAddress(resultData, context);
    
      		}
    
      	}
    
    
    
      	如果号码归属地queryAddress不等于空,就显示到号码归属地控件上:
    
      	public class MyOutGoingCall extends BroadcastReceiver {
    
      		@Override
    
      		public void onReceive(Context context, Intent intent) {
    
      			// 获取外拨电话的号码,获取广播接受者执行操作的数据
    
      			String resultData = getResultData();
    
      			// 查询号码归属地
    
      			String queryAddress = addressDao.queryAddress(resultData, context);
    
      			if (!TextUtils.isEmpty(queryAddress)) {
    
      				showToast(queryAddress);
    
      			}
    
      		}
    
      }
    

    监听外拨电话的广播接收者,需要设置权限:

      android.permission.PROCESS_OUTGOING_CALLS(Uses Permission)
    

    运行程序,在设置中心打开显示号码归属地,外拨电话显示出了“北京移动”

    代码注册广播接收者步骤总结如下:

      1.在addressservice.java中,创建一个广播接收者MyOutGoingCall
    
      2.在oncreate中new一个广播接收者,同时创建一个intentfilter,并注册广播接收者
    
      3.在ondestory方法中注销广播接收者
    
      4.在广播接收者的onreceive中执行相应操作
    

6.3归属地提示框风格 # (重点关注setSingleChoiceItems方法)

还有归属地显示,里边有归属地提示框风格:半透明,活力橙,卫士蓝,金属灰,苹果绿

这些也是对话框,只不过是单选框,现在是卫士蓝,把它改成活力橙,然后拨打电话,会显示橙色的归属地提示框



动态修改归属地提示框的风格(颜色)步骤如下:



1.创建自定义组合控件,并在布局文件中使用

  

  1)创建自定义组合控件布局settingclickview.xml

  将settingview.xml复制并改为settingclickview.xml,修改布局(将checkbox改成imagview):



		settingclickview.xml

	

		<?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="wrap_content" >

		    <TextView

		        android:id="@+id/tv_setting_title"

		        android:layout_width="wrap_content"

		        android:layout_height="wrap_content"

		        android:layout_margin="5dp"

		        android:text="提示更新"

		        android:textSize="18sp" />

		    <TextView

		        android:id="@+id/tv_setting_des"

		        android:layout_width="wrap_content"

		        android:layout_height="wrap_content"

		        android:layout_below="@id/tv_setting_title"

		        android:layout_marginLeft="5dp"

		        android:text="关闭提示更新"

		        android:textColor="#aa000000"

		        android:textSize="16sp" />

			<!-- checkbox天生带有获取焦点和点击的事件

			clickable : 设置是否可以点击   true:可以    false:不可以

			 -->

		    <ImageView

		        android:id="@+id/cb_setting_isupdate"

		        android:layout_width="wrap_content"

		        android:layout_height="wrap_content"

		        android:layout_alignParentRight="true"

		        android:layout_centerVertical="true"

		        android:layout_marginRight="18dp" 

		        android:clickable="false"

		        android:focusable="false"

		        android:src="@drawable/jiantou1"/>

		    <View

		        android:layout_width="match_parent"

		        android:layout_height="0.5dp"

		        android:layout_below="@id/tv_setting_des"

		        android:layout_marginTop="5dp"

		        android:background="#77000000" />

		</RelativeLayout>



2)创建归属地提示框风格的自定义组合控件SettingClickView



	在SettingClickView中加载布局文件settingclickview.xml

	将checkbox单选框全部换成箭头,需要将SettingClickView中关于checkbox的代码全部删掉



	public class SettingClickView extends RelativeLayout {

		

		private TextView tv_setting_title;

		private TextView tv_setting_des;

		private String des_on;

		private String des_off;



		//在代码中使用的时候调用

		public SettingClickView(Context context) {

			super(context);

			//实现不管采用哪种方式使用自定义组合控件,都能调用init()方法

			init();

		}

		//在布局文件使用的时候调用 ,多了样式文件

		public SettingClickView(Context context, AttributeSet attrs, int defStyle) {

			super(context, attrs, defStyle);

			init();

		}

		//在布局文件使用的时候调用

		public SettingClickView(Context context, AttributeSet attrs) {

			super(context, attrs);

			init();

		}

		/**
  • 添加布局文件
     /
     private void init(){
     //添加布局文件
     // TextView textView = new TextView(getContext());
     // textView.setText(“这是一个自定义组合控件的textview”);
     //第一种方式
     //将布局文件那转化成view对象
     // View view = View.inflate(getContext(), R.layout.settingview, null);//爹有了,直接找孩子,亲生的
     // //添加
     // this.addView(view);//给相对布局添加一个textview
     // this.addView(child, params);//layoutParams : 用代码给控件设置属性,就表示要设置控件在自定义空间中的属性
     //第二种方式
     //root:给view对象找一个父控件
     View view = View.inflate(getContext(), R.layout.settingclickview, this);//孩子有了,直接找爹,喜当爹,不使用,不设置属性,父控件会使用原控件的属性
     tv_setting_title = (TextView) view.findViewById(R.id.tv_setting_title);
     tv_setting_des = (TextView) view.findViewById(R.id.tv_setting_des);
     }
     /
    *

  • 设置标题

  • @param title
     /
     public void setTitle(String title){
     tv_setting_title.setText(title);
     }
     /
    *

  • 设置描述信息

  • @param des
     */
     public void setDes(String des){
     tv_setting_des.setText(des);
     }
     }

    3)布局文件中添加自定义组合控件SettingClickView

      复制SettingClickView全类名,到SettingActivity中找到activity_setting.xml,布局文件中添加自定义控件SettingClickView
    
    
    
      	activity_setting.xml
    
      
    
      	   <cn.itcast.mobilesafexian02.ui.SettingClickView
    
                  android:id="@+id/scv_setting_changedbg"
    
                  android:layout_width="match_parent"
    
                  android:layout_height="wrap_content" >
    
              </cn.itcast.mobilesafexian02.ui.SettingClickView>
    
    
    
      运行程序,归属地提示框风格条目就有了
    

    2.初始化自定义组合控件和控件,并给自定义组合控件添加点击事件

    到SettingActivity的onCreate中初始化这个条目
    
    onCreate中调用方法changedBg(),并创建这个方法
    
    在这个方法中初始化自定义控件中控件的值,给控件设置标题和描述信息
    
    
    
      private	SettingClickView scv_setting_changedbg;
    
    
    
      @Override
    
      protected void onCreate(Bundle savedInstanceState){
    
      	super.onCreate(savedInstanceState);
    
      	scv_setting_changedbg = (SettingClickView)findViewById(R.id.scv_setting_changedbg);
    
      	changedBg();
    
      }
    
    
    
      /**
    
  • 更改归属地提示框风格
     */
     private void changedBg(){

      	// 初始化自动控件中的控件的值
    
      	scv_setting_changedbg.setTitle("归属地提示框风格");
    
      	scv_setting_changedbg.setDes("半透明");
    
      	scv_setting_changedbg.setOnClickListener(new OnClickListener() {
    
    
    
      		@Override
    
      		public void onClick(View v) {
    
      					
    
      		}
    
      	});
    
      }
    
    
    
      运行程序,设置中心中“归属地提示风格的标题为"归属地提示框风格",提示信息是“半透明”,
    
      点击条目弹出单选框,单选框标题为"归属地提示框风格",图标为ic_launcher
    

    3.实现单选框功能并实现单选框的回显操作

      1)实现单选框功能
    
    
    
      	自定义组合控件的点击事件中,点击条目时显示单选框,单选框也是对话框
    
      	给对话框设置图标,标题,并实现取消按钮操作
    
    
    
      	取消按钮操作setNegativeButton(text,listener),参数listener需要onClickListener
    
      	点击取消按钮时,让对话框消失,null相当于不给按钮设置点击事件,在dialog中会默认执行dialog.dismiss();			
    
      	最后记得show()这个dialog
    
    
    
      	设置单选按钮的操作setSingleChoiceItems(items,checkedItem,listener)
    
      	参数items:单选按钮的文本数组,也就是“半透明,活力橙,卫士蓝,金属灰,苹果绿”的集合数组
    
      	参数checkedItem:当前选中的选项位置,比如选择了活力橙,那checkedItem就是活力橙的位置
    
      	参数listener:就是一个onClickListener
    
      	
    
      	单选按钮的文本数组写成:
    
      		String[] items = { "半透明", "活力橙", "卫士蓝", "金属灰", "苹果绿" };
    
      	当前选中的选项位置,先用0来表示
    
      	onClicklistener,选用DialogInterface.onClicklistener
    
      
    
      	DialogInterface.OnClickListener的onClick(DialogInterface dialog,int which)两个参数
    
      	参数dialog,是dialog
    
      	参数which:是点击选中的选择项的索引,点击金属灰,dialog消失,“归属地提示框风格”的描述信息变为了“金属灰”
    
      
    
      	在单选按钮的点击事件中:
    
      		设置自定义控件的描述信息setDes(),同时隐藏对话框
    
      		注意这里描述信息就不能写半透明了,要写的是点击选中的选项的索引,items是所有选项的文本,可以根据索引去items里边拿出相应的文本来
    
    
    
      2)实现单选框的回显操作
    
    
    
         要用到参数checkedItem,即当前选中的选项位置了,当前选中的选项用which表示,保存选中的选项涉及保存肯定用到sp
    
         保存选中的选项之后,在参数checkedItem处用sp.getInt(key,defValue),defValue默认写成0
    
    
    
      	运行程序,到设置中心点击“归属地提示框风格”条目,弹出了dialog,有标题,描述信息,还有取消,点击取消直接把对话框消失了
    
      	点击“归属地提示框风格”弹出对话框,选择活力橙对话框消失并且在“归属地提示框风格”条目的描述信息处显示“活力橙”出来了
    
    
    
      	/**
    
  • 更改归属地提示框风格
     */
     private void changedBg(){
     final String[] items = { “半透明”, “活力橙”, “卫士蓝”, “金属灰”, “苹果绿” };
     // 初始化自动控件中的控件的值
     scv_setting_changedbg.setTitle(“归属地提示框风格”);
     scv_setting_changedbg.setDes(“半透明”);
     scv_setting_changedbg.setOnClickListener(new OnClickListener() {

      			@Override
    
      			public void onClick(View v) {
    
      				AlertDialog.Builder builder = new Builder(SettingActivity.this);// 必须告诉dialog应该显示到那个activity中
    
      				// 设置图标
    
      				builder.setIcon(R.drawable.ic_launcher);
    
      				// 设置标题
    
      				builder.setTitle("归属地提示框风格");
    
      
    
      				// 设置单选按钮
    
      				// items : 单选按钮的文本的数组
    
      				// checkedItem : 当前选中的选项位置
    
      				// listener : onclickListener
    
      				builder.setSingleChoiceItems(items, sp.getInt("which",0),new DialogInterface.OnClickListener() {
    
      							// which : 点击选中的选择项的索引
    
      							@Override
    
      							public void onClick(DialogInterface dialog,
    
      									int which) {
    
      
    
      								// 保存选中的选项
    
      								Editor edit = sp.edit();
    
      								edit.putInt("which", which);
    
      								edit.commit();
    
      
    
      								// 设置自定义控件的描述信息
    
      								scv_setting_changedbg.setDes(items[which]);
    
      								// 隐藏对话框
    
      								dialog.dismiss();
    
      							}
    
      						});
    
      		
    
      				// 设置取消按钮
    
      				builder.setNegativeButton("取消", null);// null其实就相当于不给按钮设置点击事件,在dialog中会默认执行dialog.dismiss();	
    
      				// 显示dialog
    
      				builder.show();		
    
      			}
    
      		});
    
      	}
    
    
    
      运行程序,改成活力橙,再点击条目,对话框中就还是活力橙
    
      问题: 再次点击条目弹出对话框时选中的不是活力橙,变成了选中第一个半透明
    

    5.回显设置中心条目的描述信息

     刚才只是回显了单选框操作,如果退出到主界面,重新进入设置中心,发现“归属地提示框风格”条目描述信息又变成“半透明”
    
     但是点击条目弹出对话框,发现对话框中还是选中的活力橙没错
    
     同样需要根据保存的条目,从里边去获取,这里是根据保存的选项索引,去动态获取items中的文本:
    
      	scv_setting_changedbg.setDes(items[sp.getInt("which",0)]);
    
      
    
      运行程序,回显成功
    

    6.回显对应归属地框的背景颜色

     在设置中心对话框中设置完,还要去设置对应归属地框的背景颜色
    
     看下布局文件toast_custom.xml,当初设置时设置了背景颜色蓝色
    
     AddressService的showToast()中将toast_custom.xml转化成view对象之后
    
     View对象就是控件的最外层控件,所以可以给view对象去设置背景:
    
      		view.setBackgroundResource(resid)
    
     可以根据保存的选项索引从bgcolor图片数组中获取相应图片,这个图片也应该用一个数组来表示,即:bgcolor
    
     AddressService中没有这个sp,所以在onCreat中创建出来:
    
           sp = getSharedPreferences("config", MODE_PRIVATE);
    
     然后设置相应图片:view.setBackgroundResource(bgcolor[sp.getInt("which", 0)]);
    
    
    
      	/**
    
  • 显示toast
     */
     public void showToast(String queryAddress) {
     int[] bgcolor = new int[] { R.drawable.call_locate_white,
     R.drawable.call_locate_orange, R.drawable.call_locate_blue,
     R.drawable.call_locate_gray, R.drawable.call_locate_green };
     view = View.inflate(getApplicationContext(), R.layout.toast_custom,
     null);
     view.setBackgroundResource(bgcolor[sp.getInt(“which”, 0)]);
     }

      运行程序,拨打电话时,归属地框就变成想要的颜色图片背景了
    

    归属地提示框风格步骤总结如下:

      1.复制自定义控件SettingView,布局文件settingview.xml,并修改(将checkbox改成imagview),文件名改为SettingClickView,布局文件改为settingclickview.xml,同时删除关于checkbox及自定义属性的相关操作,在SettingClickView中的view.inflate中加载布局文件
    
    
    
      2.在设置中心SettingActivity的布局文件activity_setting.xml中使用自定义控件SettingClickView:
    
    
    
      	<cn.itcast.mobilesafexian02.ui.SettingClickView
    
          android:id="@+id/scv_setting_changedbg"
    
          android:layout_width="match_parent"
    
          android:layout_height="wrap_content"
    

</cn.itcast.mobilesafexian02.ui.SettingClickView> 3.在代码SettingActivity中使用 /**

  • 更改归属地提示框风格
     */
     private void changedBg() {

      		final String[] items={"半透明","活力橙","卫士蓝","金属灰","苹果绿"};
    
      		//初始化自动控件中的控件的值
    
      		scv_setting_changedbg.setTitle("归属地提示框风格");
    
      		scv_setting_changedbg.setDes(items[sp.getInt("which", 0)]);//根据保存的选项的索引,去动态获取items相应的文本
    
      		scv_setting_changedbg.setOnClickListener(new OnClickListener() {
    
      			
    
      			@Override
    
      			public void onClick(View v) {
    
      				AlertDialog.Builder builder = new Builder(SettingActivity.this);//必须告诉dialog应该显示到那个activity中
    
      				//设置图标
    
      				builder.setIcon(R.drawable.ic_launcher);
    
      				//设置标题
    
      				builder.setTitle("归属地提示框风格");
    
      				//设置单选按钮
    
      				//items : 单选按钮的文本的数组
    
      				//checkedItem : 当前选中的选项位置
    
      				//listener : onclickListener
    
      				builder.setSingleChoiceItems(items, sp.getInt("which", 0), new DialogInterface.OnClickListener() {
    
      					//which : 点击选中的选择项的索引
    
      					@Override
    
      					public void onClick(DialogInterface dialog, int which) {
    
      						//保存选中的选项
    
      						Editor edit = sp.edit();
    
      						edit.putInt("which", which);
    
      						edit.commit();
    
      						//设置自定义控件的描述信息
    
      						scv_setting_changedbg.setDes(items[which]);
    
      						//隐藏对话框
    
      						dialog.dismiss();
    
      					}
    
      				});
    
      				//设置取消按钮
    
      				builder.setNegativeButton("取消", null);//null其实就相当于不给按钮设置点击事件,在dialog中会默认执行dialog.dimiss();
    
      				//显示dialog
    
      				builder.show();
    
      			}
    
      		});
    
      	}
    
    
    
      4.根据保存的选项索引,设置addressservice中的showToast方法view的背景
    
      	a.
    
      		int[] bgcolor = new int[] { 
    
      			R.drawable.call_locate_white,
    
      			R.drawable.call_locate_orange, R.drawable.call_locate_blue,
    
      			R.drawable.call_locate_gray, R.drawable.call_locate_green };
    
      	b.
    
      		//设置view对象的背景,根据保存的选项索引从bgcolor图片数组中获取相应的图片
    
      		view.setBackgroundResource(bgcolor[sp.getInt("which", 0)]);
    

6.4更改归属地提示框显示位置 # (params.x的含义)

接下来实现归属地提示框位置功能



	要想修改控件位置就要使用params,控件在哪个父控件上显示,就去获取哪个父控件的LayoutParams,表示这个控件是符合父控件属性规则的



	想让它显示在左边,就可以在AddressService的showToast中添加如下代码:

		params.gravity = Gravity.LEFT; 

	

	运行程序,显示到了左边的位置



	想让这个控件到指定的具体某个位置,就给它指定x,y值:

			params.gravity = Gravity.LEFT | Gravity.TOP;

			params.x = 100;

			params.y = 100;

	运行程序,跑到指定位置了



	x不是坐标,是距离边框的距离,根据gravity设置的,比如将gravity改成Right:

			params.gravity = Gravity.RIGHT | Gravity.TOP;

			params.x = 100;

			params.y = 100;



	 运行程序,就变成左边比较少了,它是根据gravity来设置的

	  

	如果Gravity写成LEFT,表示距离左边框的距离,RIGHT是距离右边框的距离

	y和x是相同含义,y是根据top或bottom来设置距离顶部或底部距离

	

	更改归属地提示框位置的步骤总结如下:



	 //设置控件的显示位置, |  表示前后两个属性都有效果,两个相对应的属性,默认是左边和上边

     params.gravity = Gravity.LEFT | Gravity.TOP;

     params.x = 100;//x表示的不是坐标,而是距离边框的距离,根据Gravity来设置的,LEFT就是距离左边框的距离,ringht就是距离右边框的距离

     params.y = 100;//y和x是相同的含义,根据top或者bottom来时设置距离顶部或者底部的距离

6.5 归属地提示框位置界面 #

接下来实现归属地提示框位置界面:

上边是文字“按住提示框拖到任意位置,按手机返回键立即生效”



下边是归属地提示框,界面中的这个归属地提示框位置可以随着手指的移动而移动



先给这个界面进行布局,这个界面背景是毛玻璃效果,能隐约看到背后的界面文字



	第一:让美工做一张毛玻璃效果图片

	第二:使用android中的api实现毛玻璃效果



	使用android中的api在高版本中已经失效了

	原因:

		1.以前android手机比较卡,毛玻璃效果会让用户感觉是屏幕坏了

		2.毛玻璃效果非常耗内存,为了避免资源的浪费,谷歌在高版本上将毛玻璃效果的api给取消了

	

	所以想实现毛玻璃效果,只有让美工切图,但是图片的效果比较特殊,所以图片非常大,最少在200k-300K

	只要超过20k就非常大了,所以一般都不会用毛玻璃效果,除非特殊

	这个毛玻璃的效果就不实现了,因为没有这个图片,要是有兴趣自己拿ps去做一张玩玩





实现归属地提示框位置界面步骤:



1.在设置中心添加归属地提示框位置条目(创建自定义组合控件)



	1)到activity_setting.xml中复制一份SettingView,将id改为scv_setting_changedlocation

	然后到settingActivity中的onCreate方法中初始化这个控件,并调用"归属地提示框位置"的方法changedLocation:



			private SettingClickView scv_setting_changedlocation;

		

			@Overide

			protected void onCreate(Bundle savedInstanceState){

				super.onCreate(savedInstanceState);

		

				scv_setting_changedlocation = (SettingClickView) findViewById(R.id.scv_setting_changedlocation);

		

				changedLocation();

			}



	2)创建changedLocation方法,在方法中初始化自定义组合控件中控件的值,并设置监听

	   在onClick中“跳转到归属地提示框显示位置的界面”DragViewActivity:



			/**
  • 归属地提示框位置
     */
     private void changedLocation() {
     // 初始化自定义控件中的控件的值
     scv_setting_changedlocation.setTitle(“归属地提示框位置”);
     scv_setting_changedlocation.setDes(“设置归属地提示框的显示位置”);
     scv_setting_changedlocation.setOnClickListener(new OnClickListener() {

      				@Override
    
      				public void onClick(View v) {
    
      					// 跳转到归属地提示框显示位置的界面
    
      					Intent intent = new Intent(SettingActivity.this,
    
      							DragViewActivity.class);
    
      					startActivity(intent);
    
      				}
    
      			});
    
      		}
    
    
    
      2.创建DragViewActivity,到清单文件中配置,DragViewActivity中重写onCreate
    
    	  在onCreate中使用setContentView加载布局
    
    
    
      		public class DragViewActivity extends Activity(){
    
      			@Override
    
      			protected void onCreate(){
    
      				super.onCreate(savedInstanceState);
    
      				setContentView(R.layout.activity_dragview);
    
      		}	
    
    
    
      创建布局activity_dragview.xml
    
      对界面进行布局:
    
      	它里边有一个归属地提示框控件,在toast_custom.xml中这个控件已经做过了
    
      	直接把它拷贝到activity_dragview.xml中,将文字由“北京电信”改为“双击居中”
    
      	在整个屏幕的下边还应该有个textView来显示"按住提示框拖到任意位置\n按手机返回键立即生效"这些文字
    
      	文字中添加/n即可让文字换行显示
    
      	
    
      		<?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:orientation="vertical" >
    
      		
    
      		    <LinearLayout
    
      		        android:id="@+id/ll_dragview_toast"
    
      		        android:layout_width="wrap_content"
    
      		        android:layout_height="wrap_content"
    
      		        android:background="@drawable/call_locate_orange"
    
      		        android:gravity="center_vertical"
    
      		        android:orientation="horizontal" >
    
      		
    
      		        <ImageView
    
      		            android:layout_width="wrap_content"
    
      		            android:layout_height="wrap_content"
    
      		            android:src="@drawable/ic_launcher" />
    
      		
    
      		        <TextView
    
      		            android:id="@+id/tv_toastcustom_address"
    
      		            android:layout_width="wrap_content"
    
      		            android:layout_height="wrap_content"
    
      		            android:text="双击居中"
    
      		            android:textColor="#ffffff"
    
      		            android:textSize="18sp" />
    
      		    </LinearLayout>
    
      		    <!-- layout_alignParentBottom : 在父控件的下方 -->
    
      		    <TextView 
    
      		        android:id="@+id/tv_dragview_bottom"
    
      		        android:layout_width="match_parent"
    
      		        android:layout_height="wrap_content"
    
      		        android:text="按住提示框拖到任意位置\n按手机返回键立即生效"
    
      		        android:layout_alignParentBottom="true"
    
      		        android:textSize="18sp"
    
      		        android:textColor="#000000"
    
      		        android:gravity="center"
    
      		        android:background="@drawable/call_locate_blue"
    
      		        android:padding="10dp"/>
    
      		    <TextView 
    
      		         android:id="@+id/tv_dragview_top"
    
      		        android:layout_width="match_parent"
    
      		        android:layout_height="wrap_content"
    
      		        android:text="按住提示框拖到任意位置\n按手机返回键立即生效"
    
      		        android:textSize="18sp"
    
      		        android:textColor="#000000"
    
      		        android:gravity="center"
    
      		        android:background="@drawable/call_locate_blue"
    
      		        android:padding="10dp"
    
      		        android:visibility="invisible"/>
    
      		</RelativeLayout>
    
    
    
      运行程序,设置中心点击归属地提示框位置,直接进入了DragViewActivity界面了
    

6.6 随着手指移动而移动之设置触摸监听事件 # (重点!!!)

随着手指移动而移动的操作:

	1:按住控件,记录开始x和y坐标,startX,startY,这个坐标是手指按住的坐标

	2:移动,获取移动到的新位置的x和y坐标,newX,newY

	3:计算移动偏移量 dX = newX - startX,dY= newY-startY;(计算移动偏移量就是计算移动距离)

	4:让控件移动相应偏移量,重新绘制控件

	5:更新开始x和y坐标 startX = newX, startY = newY

	

	要按住控件移动,先要给控件添加id为ll_dragview_toast,在DragViewActivity的onCreate中初始化id,并给控件设置触摸监听事件setOnTouchListener:



		public class DragViewActivity extends Activity(){

			private LinearLayout ll_dragview_toast;

			@Override

			protected void onCreate(){

				super.onCreate(savedInstanceState);

				setContentView(R.layout.activity_dragview);

				ll_dragview_toast = (LinearLayout) findViewById(R.id.ll_dragview_toast);

				//给控件设置触摸监听事件

				setTouch();

		}

		

		/**

		 *设置触摸监听事件

		 */

		private void setTouch(){

			ll_dragview_toast.setOnTouchListener(new OnTouchListener() {

				@Override

				public boolean onTouch(View v,MotionEvent event){	

					return false;

				}

			});	

		  }

		}



	onTouch方法,参数v:代表的是控件,参数event:代表的是当前执行的事件

	当前事件有:ACTION_DOWN(按下的事件)

			   ACTION_MOVE(移动的事件)

			   ACTION_UP(抬起的事件)



	/**

	 *设置触摸监听事件

	 */

	private void setTouch(){

		ll_dragview_toast.setOnTouchListener(new OnTouchListener() {

			

			@Override

			public boolean onTouch(View v,MotionEvent event){

				//获取当前的事件	

				switch(event.getAction()){

					case MotionEvent.ACTION_DOWN:

						//按下的事件

						System.out.println("按下了.....");

						break;

					case MotionEvent.ACTION_MOVE:

						//移动的事件

						System.out.println("移动了.....");

						break;

					case MotionEvent.ACTION_UP:

						//抬起的事件

						System.out.println("抬起了.....");

						break;		

				}

				return false;

			}

		});	

	  }

	}



	运行程序,设置中心点击归属地提示框位置,点击“双击居中”控件,在logCat中就显示"按下了....."

	移动控件发现没有移动成功也没打印“移动了.....”,手指抬起也没有打印“抬起了.....”



	原因:将鼠标放在onTouch方法,解释:

			returns:true if the listener has cosumed the envent,false otherwise

	意思是true的话表示消费掉了,执行了,false表示不执行

	

	这里的不执行不是说不执行事件,而是不执行下一个事件,在触摸事件中,按下、移动、抬起,它都是单独的事件

	比如按下了,就会打印“按下了...”,紧接着上边就返回了false,false意思是按下这个事件还没有执行完,还不能去执行移动事件,

	

	所以最后return要返回true,当按下了,打印出“按下了...”了,下边返回true,表示按下事件执行完了,可以去执行移动和抬起事件了:



	/**

	 *设置触摸监听事件

	 */

	private void setTouch(){

		ll_dragview_toast.setOnTouchListener(new OnTouchListener() {

			

			@Override

			public boolean onTouch(View v,MotionEvent event){

				//获取当前的事件	

				switch(event.getAction()){



				case MotionEvent.ACTION_DOWN:

					//按下的事件

					System.out.println("按下了.....");

					break;

				case MotionEvent.ACTION_MOVE:

					//移动的事件

					System.out.println("移动了.....");

					break;

				case MotionEvent.ACTION_UP:

					//抬起的事件

					System.out.println("抬起了.....");

					break;		

				}

				//True if the listener has consumed the event, false otherwise.

				//true:消费了,执行了,false:不执行

				//返回true表明我们down已经执行完了,可以执行移动和抬起的事件了

				return true;

			}

		});	

	  }

	}

	

	再运行程序,重复上边的操作,打印log,就可以打印出相应的“移动了......”,“抬起了.......”

6.7 随着手指移动而移动之实现 #

第一步:记录开始x和y的坐标startX,startY,即在ACTION_DOWN里执行:

	上边获取startX,startY坐标最重要的就是event下的getRawX(),getRawY()方法

第二步:移动时获取新位置的x和y的坐标newX,newY,并执行第三步计算移动偏移量:

	上边获取newX,newY最重要的还是event下的getRawX(),getRawY()方法

第三步:让控件移动相应偏移量,重新绘制控件,ll_dragview_toast有个方法:layout(l,t,r,b);

就是重新绘制控件

	参数l: 表示控件距离屏幕左边的距离

	参数t: 表示控件距离屏幕上边的距离

	参数r: 表示控件右边框距离屏幕左边的距离

	参数b: 表示控件下边框距离屏幕上边的距离



	参数l,参数t有了,可以变相以为就是x和y,控件都是以左上角为圆心,控件左上角的那个点用x,y表示

	那x距离就相当于L,y距离就相当于t



	要移动相应偏移量,移动的偏移量是dX和dY,说白了要移动相应偏移量直接改变L和T的值就可以了:

				//移动相应的偏移量

				l+=dX;

				t+=dY;

	l和t有了之后,再看r和b,R的值就相当于L的值加上控件宽度,B的值就相当于T加上控件高度



第四步:更新开始的坐标,让每次都是新的位置



代码如下:



	/**

	 *设置触摸监听事件

	 */

	private void setTouch(){

		ll_dragview_toast.setOnTouchListener(new OnTouchListener() {

			

			//设置成成员变量是为了在移动的事件能够进行使用

			private int startX;

			private int startY;

			

			@Override

			public boolean onTouch(View v,MotionEvent event){

				//获取当前的事件	

				switch(event.getAction()){



				case MotionEvent.ACTION_DOWN:

					//按下的事件

					System.out.println("按下了.....");

					startX = (int) event.getRawX();

					startY = (int) event.getRawY();

					break;

				case MotionEvent.ACTION_MOVE:

					//移动的事件

					System.out.println("移动了.....");

					

					//2.获取新的位置的x和y的坐标

					int newX = (int) event.getRawX();

					int newY = (int) event.getRawY();

					//3.计算移动的偏移量

					int dX = newX-startX;

					int dY = newY-startY;

					//4.让控件移动相应的偏移量,重新绘制控件

					int l = ll_dragview_toast.getLeft();//获取控件距离父控件左边框的距离,原控件(没移动前)的距离

					int t = ll_dragview_toast.getTop();//获取控件距离父控件顶部的距离

					//移动相应的偏移量

					l+=dX;

					t+=dY;



					ll_dragview_toast.layout(l, t, l+ll_dragview_toast.getWidth(), t+ll_dragview_toast.getHeight());//重新绘制控件

					

					//5.更新开始的坐标,为了让每次都是新的位置

					startX = newX;

					startY = newY;



					break;

				case MotionEvent.ACTION_UP:

					//抬起的事件

					System.out.println("抬起了.....");

					break;		

				}

				//True if the listener has consumed the event, false otherwise.

				//true:消费了,执行了,false:不执行

				//返回true表明我们down已经执行完了,可以执行移动和抬起的事件了

				return true;

			}

		});	

	  }

	}



运行程序,已经可以随着手指移动而移动了

6.8 动态设置号码归属地提示框位置 #

随着手指移动而移动的操作是为了在设置中心的“归属地提示框位置”中将“归属地提示框”移动到哪,在来电或拨打电话时,“归属地提示框”就在哪个位置显示



	DragViewActivity的setTouch中的onTouch中抬起事件ACTION_UP中去保存控件坐标,也是用sp来实现

	先在DragViewActivity的onCreate将sp创建,文件名称name写成"config",mode写成MODE_PRIVATE:



		private SharedPreferences sp;

		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

			sp = getSharedPreferences("config", MODE_PRIVATE);

		}



	保存Int类型x,y值,x就是L,y就是T,保存的是控件坐标不是手指坐标,控件坐标指的是控件左上角的这一点,那x和y可以在这里去获取下:



		case MotionEvent.ACTION_UP:

			//抬起的事件

			System.out.println("抬起了.....");

			//获取坐标

			int x = ll_dragview_toast.getLeft();//获取移动过的控件的距离屏幕左边框的距离

			int y = ll_dragview_toast.getTop();

			//保存控件坐标,而不是手指坐标

			Editor edit = sp.edit();

			edit.putInt("x", x);

			edit.putInt("y", y);

			edit.commit();

			break;



	这里获取到的x,y是移动后的坐标,保存成功控件坐标后要去回显x,y



	来到AddressService中showToast中,这里写的是:

				params.x = 100; 

				params.y = 100;

	此时可以替换成sp中保存的x,y:

		params.x = sp.getInt("x", 100);// x表示不是坐标,而是距离边框距离,根据Gravity设置,LEFT就是距离左边框距离,ringht是距离右边框距离

		params.y = sp.getInt("y", 100);// y和x相同含义,根据top或bottom设置距离顶部或底部距离



	sp这里的100指的是默认值defValue



	运行程序,将设置中心的归属地提示框移动到右上,拨打电话,归属地提示框就显示在了拨打电话界面的右上了

6.9 回显位置 # (重点)

上边已实现控件随着手指移动而移动,将坐标也保存成功,但将设置中心中“归属地提示框位置”中的控件移动到右边,点击返回键,再点击“归属地提示框位置”条目进去,发现控件又回到了左上角位置,没有进行回显



已将x,y坐标用sp保存成功,在DragViewActivity中的onCreate去回显sp中保存的x,y坐标:



	public class DragViewActivity extends Activity {



		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

			//获取保存的x和y的坐标

			int x = sp.getInt("x", 0);

			int y = sp.getInt("y", 0);

			System.out.println("x:"+x+"   y:"+y);

		}

	}



有x,y坐标了,回显位置说白了就是重新绘制控件,DragViewActivity的ACTION_MOVE的重新绘制控件是layout方法:

	ll_dragview_toast.layout(l, t, l+ll_dragview_toast.getWidth(), t+ll_dragview_toast.getHeight());//重新绘制控件



在DragViewActivity的onCreate中也实现下重新绘制控件:



	public class DragViewActivity extends Activity {

		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

	

			//获取保存的x和y的坐标

			int x = sp.getInt("x", 0);

			int y = sp.getInt("y", 0);

			System.out.println("x:"+x+"   y:"+y);

			//重新绘制控件

			ll_dragview_toast.layout(l,t,r,b);

			}

	}



	参数l,t就是x,y,因为在抬起事件ACTION_UP中保存时,x,y分别获取的getLeft(),getTop()

	参数r就是l+控件宽度,先把控件宽高获取出来:



	public class DragViewActivity extends Activity {



	@Override

	protected void onCreate(Bundle savedInstanceState) {

		super.onCreate(savedInstanceState);



		//获取保存的x和y的坐标

		int x = sp.getInt("x", 0);

		int y = sp.getInt("y", 0);

		System.out.println("x:"+x+"   y:"+y);

		//获取控件的宽高

		int width = ll_dragview_toast.getWidth();

		int height = ll_dragview_toast.getHeight();

		System.out.println("width:"+width+"  height:"+height);

		//重新绘制控件 第二步操作,第一步操作没有完成,第二步是不会执行的,同样第三步也不会执行

		ll_dragview_toast.layout(x,y,x+width,y+height);

	}

}



运行程序,点击设置中心中归属地提示框位置,将控件移动到右边,点击返回键,再点击归属地提示框位置条目,看到没有进行回显操作,控件还是回到了左上角位置



那看下log,x:140,y:173,已经获取出来保存的值,但是width:0,height:0



	android中所有控件要正常显示分为三步骤:

		1.测量控件宽高,oncreate执行完,测量控件才会完成

		2.分配控件位置

		3.绘制控件



在oncreate中只是去加载了下布局,然后开始去给布局进行初始化操作,要想让这个控件显示出来,必须要经过上边3步才能完成,onCreate执行完时测量控件才会完成,紧接着才会执行第2步分配控件位置,第3步绘制控件操作



这就是在onCreate中获取出来的宽度,高度为0的原因,为0直接影响了控件没办法进行回显操作



在android中如果第一步没有执行完,第二步是不会去执行的,第二步没有执行完,第三步也不会去执行



layout方法解释是this is a second phase,说明是第二步操作

第一步还没执行完,当然不会执行第二步layout方法了,更不会执行第三步绘制控件操作了

所以在onCreate中来做这个不行,把它注释掉:



	public class DragViewActivity extends Activity {



		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

	

			//获取保存的x和y的坐标

			int x = sp.getInt("x", 0);

			int y = sp.getInt("y", 0);

			System.out.println("x:"+x+"   y:"+y);



			//获取控件的宽高

//			int width = ll_dragview_toast.getWidth();

//			int height = ll_dragview_toast.getHeight();

//			System.out.println("width:"+width+"  height:"+height);

//			//重新绘制控件 第二步操作,第一步操作没完成,第二步不会执行,同样第三步也不会执行

//			ll_dragview_toast.layout(x,y,x+width,y+height);

		}

	}



	在AddressService中:

		params.x = sp.getInt("x", 100);

		params.y = sp.getInt("y", 100);



	在AddressService中执行上边的params.x,y,可以显示出sp中位置,我也去获取一个params

	回到DragViewActivity的onCreate中,通过控件id去获取getLayoutParams(),它是获取控件属性



	public class DragViewActivity extends Activity {



		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

	

			//获取保存的x和y的坐标

			int x = sp.getInt("x", 0);

			int y = sp.getInt("y", 0);

			System.out.println("x:"+x+"   y:"+y);



			RelativeLayout.LayoutParams params = (LayoutParams) ll_dragview_toast.getLayoutParams();//获取控件的属性参数



		}

	}



前边在AddressService的showToast的设置toast属性处说过:

	设置toast属性,LayoutParams代表控件属性,你的控件在哪个父控件上显示,就要获取哪个父控件的LayoutParams,表示这个控件符合父控件的属性规则



	来到activiy_dragview.xml中,linearlayout是在父控件相对布局Relativelayout中,这里获取到的LayoutParams就是RelativeLayout的



	已经获取到这个RelativeLayout控件的参数params,里边有topMargin,leftMargin,把x值设置给leftMargin,y值设置给topMargin:



	public class DragViewActivity extends Activity {



	@Override

	protected void onCreate(Bundle savedInstanceState) {

		super.onCreate(savedInstanceState);



		//获取保存的x和y的坐标

		int x = sp.getInt("x", 0);

		int y = sp.getInt("y", 0);

		System.out.println("x:"+x+"   y:"+y);



		RelativeLayout.LayoutParams params = (LayoutParams) ll_dragview_toast.getLayoutParams();//获取控件的属性参数

		

		//设置控件距离父控件左边框的距离

		params.leftMargin = x;

		//设置控件距离父控件顶部的距离

		params.topMargin = y;

	}

}



接下来重新设置控件属性,前边在AddressService中的showToast中说过,重新设置属性是setLayoutParams(params);:



	public class DragViewActivity extends Activity {



		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

	

			//获取保存的x和y的坐标

			int x = sp.getInt("x", 0);

			int y = sp.getInt("y", 0);

			System.out.println("x:"+x+"   y:"+y);



			RelativeLayout.LayoutParams params = (LayoutParams) ll_dragview_toast.getLayoutParams();//获取控件的属性参数

			

			//设置控件距离父控件左边框的距离

			params.leftMargin = x;

			//设置控件距离父控件顶部的距离

			params.topMargin = y;

			//重新设置控件的属性

			ll_dragview_toast.setLayoutParams(params);

	

		}

	}



运行程序,直接回显位置了,再拖动到右上,返回再进去还是在右上

回显位置的操作就做完了,这里必须使用LayoutParams实现





回显归属地提示框位置操作总结:



	1.获取保存的x和y的坐标

		int x = sp.getInt("x", 0);

		int y = sp.getInt("y", 0);

		System.out.println("x:"+x+"   y:"+y);



	2.获取父控件的属性,并设置相应的属性

		RelativeLayout.LayoutParams params = (LayoutParams) ll_dragview_toast.getLayoutParams();//获取控件的属性参数

		//设置控件距离父控件左边框的距离

		params.leftMargin = x;

		//设置控件距离父控件顶部的距离

		params.topMargin = y;

		//重新设置子控件的属性

		ll_dragview_toast.setLayoutParams(params);



问题:	

	把设置中心中的归属地提示框移动到界面外边去,再返回,再进去,发现控件变形压缩了



	控件变形原因:

	保存的控件坐标是左上角坐标,当控件移出屏幕再进行回显操作重新绘制控件时发现现存的空间放不下控件,所以控件自作聪明对自己进行压缩处理,以便让用户能够完全看到自己



解决控件移出屏幕问题:

	判断一下,当移动到左边框或右边框位置时,就不让再移出屏幕了



	到DragViewActivity的ACTION_MOVE下的移动事件中判断控件是否移出屏幕

	怎么判断控件是否移出屏幕?

		如果控件左边距离屏幕左边为0,就不让控件向左移动了,也就是L值小于0就不让向左边移动了

		如果控件右边距离屏幕右边为0,就不让控件向右移动了,也就是R值大于屏幕宽度不让向右边移动

	

	这里没有r,那我把重新绘制控件这行代码:

		ll_dragview_toast.layout(l, t, l+ll_dragview_toast.getWidth(), t+ll_dragview_toast.getHeight());//重新绘制控件



	写成:

		int r = l+ll_dragview_toast.getWidth();

		int b = t+ll_dragview_toast.getHeight();

		ll_dragview_toast.layout(l, t, r, b);//重新绘制控件

	

	这样判断中就有了r,r大于屏幕宽度,那这个屏幕宽度没有

	到DragViewActivty中的onCreate中获取,通过getSystemService(name)获取windowManager

	里边有一个getDefaultDisplay(),方法中有个getWidth(),这个就可以获取到宽度了:



		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

			//获取屏幕宽度

			WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

			int width = windowManager.getDefaultDisplay().getWidth();

		}



	getWidth()上有一个斜杠,表示过时了,可以用的,还有另一种方式,windowManager中有这样一个方法,叫做getDefaultDisplay(),下边有个getMetrics(outMetrics),这个方法中需要一个DisplayMetrics,可以new出来一个DisplayMetrics:



		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

			//获取屏幕宽度

			WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

	//		int width = windowManager.getDefaultDisplay().getWidth();

			DisplayMetrics outMetrics = new DisplayMetrics();//白纸

			windowManager.getDefaultDisplay().getMetrics(outMetrics);//给白纸指定宽高

			

		}



	DisplayMetrics相当于白纸,下边操作给白纸指定宽高,从白纸中获取出宽度也可以获取出高度,这样屏幕的宽度就获取出来了,申明成成员变量,方便下边判断中使用:



		private int width;

		private int height;

	

		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

	

			//获取屏幕宽度

			WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

	//		int width = windowManager.getDefaultDisplay().getWidth();

			DisplayMetrics outMetrics = new DisplayMetrics();//白纸

			windowManager.getDefaultDisplay().getMetrics(outMetrics);//给白纸指定宽高

			width = outMetrics.widthPixels;

			height = outMetrics.heightPixels;

		}



	回到DragViewActivity下边的setTouch中的ACTION_MOVE的判断控件是否移出屏幕中,添加r>width的判断,当r大于width时不让控件移动直接break就行:



		case MotionEvent.ACTION_MOVE:

	

			int r = l+ll_dragview_toast.getWidth();

			int b = t+ll_dragview_toast.getHeight();

			//判断控件是否移出屏幕

			if (l < 0 || r > width) {

				break;

			}

			//r:表示控件右边框距离屏幕左边框的距离

			ll_dragview_toast.layout(l, t, r, b);//重新绘制控件

	

			break;

	

	这个判断还没有判断完, 还有一个上下:

	那上是t,t<0和l<0是一样的操作,也不让控件再向上移动了

	当b>屏幕高度时,这个高度还包括通知栏这部分,为了避免程序出问题,要将通知栏高度减去,用测量工具测量一下,测量出来大概是25,那这里就该写成b>height-25,即如果b>height-25,也不让控件再向下移动了:



		case MotionEvent.ACTION_MOVE:

	

			int r = l+ll_dragview_toast.getWidth();

			int b = t+ll_dragview_toast.getHeight();

			//判断控件是否移出屏幕

			if (l < 0 || r > width || t < 0 || b > height - 25) {

				break;

			}

			//r:表示控件右边框距离屏幕左边框的距离

			ll_dragview_toast.layout(l, t, r, b);//重新绘制控件

	

			break;



	运行程序,设置中心点击归属地提示框位置条目,将控件向屏幕右边移动移不出去,向左移动也移不出去,这就解决了控件移出屏幕问题



解决控件移出屏幕问题代码总结如下:



	1.DragViewActivity中的setTouch中的ACTION_MOVE的移动事件中,在重新绘制控件之前增加代码:

		//判断控件是否移出屏幕

		if (l < 0 || r > width || t < 0 || b > height - 25) {

				break;

		}



	  因为需要用到屏幕宽高,所以

	2.获取屏幕宽高

		//获取屏幕宽度

		WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

		//int width = windowManager.getDefaultDisplay().getWidth();

		DisplayMetrics outMetrics = new DisplayMetrics();//白纸

		windowManager.getDefaultDisplay().getMetrics(outMetrics);//给白纸指定宽高

		width = outMetrics.widthPixels;

		height = outMetrics.heightPixels;



	到这,回显位置的操作做完了

6.8多击事件#(重点,原理)

控件上还有一个“双击居中”文字,实现下双击操作

	DragViewActivity的onCreate中调用doubleClick(),创建出这个方法

	先来实现单击事件,再来实现双击事件,单击事件写法如下:

		private void doubleClick() {

			ll_dragview_toast.setOnClickListener(new OnClickListener() {

				@Override

				public void onClick(View v) {

					System.out.println("单击了......");

				}

			});

		}



	运行程序,单击控件发现没有打印“单击了...”,说明没有走这个单击事件的方法,打印的是“按下...”和“抬起...”



		单击事件和触摸事件区别:

		1.单击事件 : 是一组事件的集合,按下+抬起表示一个事件

		2.触摸事件 : 单个事件,按下就是按下事件,抬起就是抬起事件



		所以单击事件和触摸事件一起使用时,触摸事件要返回return  false,不能返回return true;



	将DragViewActivity的触摸事件onTouch中返回的true改为false

	运行程序,点击控件,log中输出了“按下了...”,"抬起了...","单击了...",但是在执行单击前先执行了按下和抬起,意思是在执行单击事件之前,先执行了触摸事件

	

	原因是先执行的触摸事件,再执行单击事件,触摸事件中返回fasle表示事件拦截了(没有执行),但是在单击事件中按下事件是单击事件前半部分,为了单击事件能成功在单击事件中把按下的事件处理了

	相当于ontouch事件返回false,表示按下事件被拦截没有执行,但是紧接着执行了单击事件,单击事件认为按下是前半部分,下半部分还要执行抬起事件,相当于又把单击事件给消费执行了,就能够执行触摸事件的移动操作了



	所有和触摸控件有关操作,都是先执行触摸事件,再执行其他事件

	也可以看下onClick源码,所有触摸控件内部都是通过触摸事件实现



	按下ACTION_DOWN时获取时间,抬起ACTION_UP时获取时间,这两个时间小于100毫秒就认为是单击事件

	所有操作都是通过触摸事件的onTouch实现,这也是onTouch中的这些操作可以被点击事件消费的原因

	因为点击事件就是基于触摸事件的onTouch实现的



	单击能执行了,接下来就能执行双击操作了

	安卓中没有双击事件方法,但是可以用另外一种方式实现



	资料里有packages/apps/Settings,将Settings拷贝到桌面并导入到eclipse中,导入时有一个test测试类可不选,这个Settings项目在eclipse中没法编译,涉及到一些底层资源,必须进行源码编译才能编译,源码编译会在后边提下,应该是在linux下进行源码编译的课程

		

	这里不能编译运行但不妨碍来看,模拟器中的Settings中有个About phone,它下边有个Android version,选中eclispe的Settings项目,ctrl+H,输入Android version,去xml文件中找,将Choose...的java改成.xml,点击搜索

	找到strings.xml,可以一路追踪到device_info_settings.xml,然后追踪到DeviceInfoSettings.java,在它里边找多击事件,找到onPreferenceTreeClick:



	DeviceInfoSettings.java



		@Override

		public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,Preference preference){

			if(preference.getKey().equals("firmware_version")){

				System.arraycopy(mHits,1,mHits,0,mHits.length-1);

				mHits[mHits.length-1] = SystemClock.uptimeMillis();

				if(mHits[0] >=(SystemClock.uptimeMills()-500)){

					Intent intent = new Intent(Intent.ACTION_MAIN);

					intent.setClassName("android",

						com.android.internal.app.PlatLogoActivity.class.getName());

				try{

					startActivity(intent);

				} catch(Exception e){

				}

			}

		}

		return super.onPreferenceTreeClick(preferenceScreen,preference);

		}



	这里是谷歌开发师大意了,应该写成Three少写了h还不愿意改

	在intent中有PlatLogoActivity.class.getName(),PlatLogoActivity就是弹出的logo界面,也就是多击时,执行了上边操作,如果符合这个条件就跳转到logo界面,也就说intent上边这几行代码就是多击操作



	把这几行代码拷贝到自己项目中研究下,先写一个示例工程名称:三击



	在activity_main.xml中增加button按钮,button按钮中增加点击事件threeClick,MainActivity中实现方法,将多击那几行代码拷贝到这个方法中,拷贝进去有错误没事,回到Settings的DeviceInfoSettings.java中,找下mHits是一个long数组,拷贝过来:



		long[] mHits = new long[3];

		public void threeClick(View v){

			//拷贝数组的操作	

			System.arraycopy(mHits,1,mHits,0,mHits.length-1);

			mHits[mHits.length-1] = SystemClock.uptimeMillis();//获取距离开机时间毫秒值

			if(mHits[0] >=(SystemClock.uptimeMills()-500)){

				System.out.prinln(三击了...);

		}

	}



	运行三击程序,快速点击“三击”按钮,打印出“三击了...”



	三击原理:

		看第一行代码,arraycopy是拷贝数组操作:

			第一个参数是拷贝的原数组

			第二个参数是从原数组的哪个位置开始拷贝

			第三个参数是拷贝的目标数组

			第四个参数是从目标数组的哪个位置开始去写数据

			第五个参数是拷贝的长度



	这行代码意思是要拷贝的原数组就是这个long类型数组,从第一个位置拷贝,拷贝到mHits原数组

	往后看,从这个数组的0位置去写,拷贝长度是长度减1,也就是拷贝2个长度



	第一击有个长度为3数组,拿到数组执行arraycopy代码时,就说要拷贝这个数组,从第一个位置拷贝,还是拷贝到这个数组中,从0位置开始去写,将这个数组的第1位置的数据放到第0位置上,因为最后它是拷贝两个长度,也就说第2个位置的数据放到第1位置上,这个是拷贝数据操作



	再往下看,它获取了距离开机时间的一个毫秒值,也就是距离开机的一个时间,给了mHits这个数组长度减1的元素,3-1是第2个元素,比如说第2位置得到了一个500

	

	继续往下看代码,它是拿第0个的元素继续进行判断了,和什么判断呢?它是再次获取了一个距离开机时间减去500

	第一个(位置为0)里边没有数据,没有数据且是long类型的,那就是0,0大于不了任何数,所以打印“三击了...”这段代码是肯定不会走的,这是第一击



	来看第二击,第二击同样还是这个数组,还是三份,里边还是0,1,2,然后紧接着又开始执行刚才的操作了,首先将第1位置的数据拷贝到第0位置,然后将第2位置的数据拷贝到第1位置,注意在拷贝前,这时候第二位置有个500的值,所以把第二位置里边的元素数值500拷贝到了第1位置里边,那第1位置里边就变成500了,然后紧接着又执行“获取距离开机时间的一个毫秒值”这段代码了,并将值获取出来赋值给第2个元素,也就说第二位置不是500了,得到了一个700,这是第2击,这时候第0位置还是0,0不大于开启时间,所以还是不 

	会执行打印“三击了...”这段代码



	来看第三击,第三击时也是执行的这样的操作,将第1位置的数据拷贝到第0位置,在没有拷贝前第1位置已经是500了,拷贝之后第0位置就变成500了,然后将第2位置的数据700拷贝到第1位置,第一位置就变成了700,然后又该执行“获取距离开机时间的一个毫秒值”这段代码来获取下距离开机的时间,比如获取到的是900,接下来该执行if判断了,现在第0位置已经是500了,然后又获取下距离开机的毫秒值并减500,上边获取到的是900,那这里比如获取到的是1000,1000-500也等于500,那if判断的结果是第0位置的500等于500,这时就会执行打印“三击了...”这段代码



	这个就是整个3击的流程,安卓中不管你是从2击开始做多少击,全部都是按照这个原理来执行的



	注意,这里“获取距离开机时间的一个毫秒值”,这个包括手机睡眠的时间,你开机之后只要一直不关机,这个值会越来越大的



	那三击时,点了3次这个按钮,每点一次会执行下threeClick点击事件,点第三次时如满足if判断就打印“三级了...”,不满足就认为不是三击,不会执行打印“三级了...”



	三击效果实现了,把它拷贝到手机卫士西安02的DragViewActivity中的doubleClick中,将这个数组的长度改成2,就变成了2击



	在if判断中就可以执行双击之后想要的效果了

	双击之后要居中,用layout(l,t,r,b),因为在点击时,控件ll_dragview_toast已经有了,说明它重新绘制控件成功了,1,2,3步已经走完了,走完了就可以再次走这个方法了,即因为点击控件进行操作的,所以控件已经有了,那就可以去执行重新绘制操作,这里需要l,t,r,b



	先获取一个l的值,l的值为屏幕的宽度width减去控件的宽度,然后除以2,

	t的值为屏幕的高度减去控件的高度,再减去通知栏的高度25,最后除以2

	r的值等于l加上控件的宽度width,b的值等于t加上控件的高度



	运行程序,查看效果,双击之后控件就居中显示了,这里执行完了之后不要忘了保存坐标,为回显操作做准备,这里回显操作可以直接拷贝本java文件中上边的回显操作,然后将值x,y改为l,t:



	DragViewActivity.java



	long[] mHits = new long[2];

	

	/**
  • 双击事件
     */
     private void doubleClick() {
     ll_dragview_toast.setOnClickListener(new OnClickListener() {

      		@Override
    
      		public void onClick(View v) {
    
    
    
      			//拷贝数组的操作
    
      			System.arraycopy(mHits, 1, mHits, 0, mHits.length-1);
    
      	        mHits[mHits.length-1] = SystemClock.uptimeMillis();//获取距离开机时间的毫秒值(手机睡眠的时间)
    
      	        if (mHits[0] >= (SystemClock.uptimeMillis()-500)) {//判断是否是第三次点击
    
    
    
      	        	//双击居中,因为点击控件进行操作的,所以控件已经有了,那就可以去执行重新绘制操作
    
      	        	int l = (width - ll_dragview_toast.getWidth())/2;
    
      	        	int t = (height - ll_dragview_toast.getHeight() -25)/2;
    
      	        	ll_dragview_toast.layout(l, t, l+ll_dragview_toast.getWidth(), t+ll_dragview_toast.getHeight());
    
    
    
      	        	//保存坐标,为回显做准备
    
      	        	Editor edit = sp.edit();
    
      				edit.putInt("x", l);
    
      				edit.putInt("y", t);
    
      				edit.commit();
    
      	        	
    
      	        }
    
      		}
    
      	});
    
      }
    
    
    
      运行程序,双击居中之后退出界面再进去,控件还是居中显示,说明回显成功
    
      注意:这里边为了让控件居中显示,l,t取值这个知道为什么这么写不?
    
      画个图看下,最终确定了控件左上角的位置,那控件的位置也就确定了
    

6.11 细节的处理 #

功能:控件移动到最下边textView文字部分时,文字部分就会跑到屏幕最顶部,控件再移动到屏幕最顶部时文字部分又会回到屏幕最底部



实现:上边一个textView,下边一个textView,判断getTop值,如果控件移动到屏幕高度的一半了,下边隐藏,上边显示,小于一半时,上边隐藏,下边显示



1.布局文件中设置两个一模一样的textview,分别显示在父控件上方和下方



	activity_dragview.xml



	<?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:orientation="vertical" >

	    <LinearLayout

	        android:id="@+id/ll_dragview_toast"

	        android:layout_width="wrap_content"

	        android:layout_height="wrap_content"

	        android:background="@drawable/call_locate_orange"

	        android:gravity="center_vertical"

	        android:orientation="horizontal" >

	        <ImageView

	            android:layout_width="wrap_content"

	            android:layout_height="wrap_content"

	            android:src="@drawable/ic_launcher" />

	        <TextView

	            android:id="@+id/tv_toastcustom_address"

	            android:layout_width="wrap_content"

	            android:layout_height="wrap_content"

	            android:text="双击居中"

	            android:textColor="#ffffff"

	            android:textSize="18sp" />

	    </LinearLayout>

	    <!-- layout_alignParentBottom : 在父控件的下方 -->

	    <TextView 

	        android:id="@+id/tv_dragview_bottom"

	        android:layout_width="match_parent"

	        android:layout_height="wrap_content"

	        android:text="按住提示框拖到任意位置\n按手机返回键立即生效"

	        android:layout_alignParentBottom="true"

	        android:textSize="18sp"

	        android:textColor="#000000"

	        android:gravity="center"

	        android:background="@drawable/call_locate_blue"

	        android:padding="10dp"

	        />

	    <TextView 

	         android:id="@+id/tv_dragview_top"

	        android:layout_width="match_parent"

	        android:layout_height="wrap_content"

	        android:text="按住提示框拖到任意位置\n按手机返回键立即生效"

	        android:textSize="18sp"

	        android:textColor="#000000"

	        android:gravity="center"

	        android:background="@drawable/call_locate_blue"

	        android:padding="10dp"

	        android:visibility="invisible"

	        />

	</RelativeLayout>



	将两个控件tv_dragview_bottom,tv_dragview_top在DragViewActivity的onCreate初始化出来

	移动控件时判断,DragViewActivity的setTouch中的ACTION_MOVE中,重新绘制控件后去执行判断



2.在移动事件ACTION_MOVE进行判断



		//绘制控件之后执行控件textivew显示隐藏操作

		//1.获取控件距离父控件顶部的距离

		int top = ll_dragview_toast.getTop();

		//2.判断是否大于屏幕高度的一半

		if (top > height/2) {

			//下方隐藏,上方显示

			tv_dragview_bottom.setVisibility(View.INVISIBLE);

			tv_dragview_top.setVisibility(View.VISIBLE);

		}else{

			//上方隐藏,下方显示

			tv_dragview_top.setVisibility(View.INVISIBLE);

			tv_dragview_bottom.setVisibility(View.VISIBLE);

		}



	第一步获取控件距离父控件顶部距离,这个操作做过,其实就是ll_dragview_toast.getTop();

	最后不要忘了,这里也要有回显操作,DragViewActivity的onCreate中将上边代码复制一份:



3.回显时也进行判断

		DragViewActivity.java

	

		@Override

		protected void onCreate(Bundle savedInstanceState) {

			super.onCreate(savedInstanceState);

	

			//设置回显时textivew的显示和隐藏

			if (y > height/2) {

				//下方隐藏,上方显示

				tv_dragview_bottom.setVisibility(View.INVISIBLE);

				tv_dragview_top.setVisibility(View.VISIBLE);

			}else{

				//上方隐藏,下方显示

				tv_dragview_top.setVisibility(View.INVISIBLE);

				tv_dragview_bottom.setVisibility(View.VISIBLE);

			}

		}	



	top就是y,所以回显时将top改为y,当初sp中保存y时保存的就是getTop()值

6.12 电话界面控件随着手指移动而移动 # (重点)

将设置中心“显示号码归属地”勾选上,将“归属地提示框位置”控件移动到中下方,外拨电话,遮住了挂断电话

实现功能:让外拨电话界面和拨打电话界面中的“来电归属框”可以移动



	用到的也就是onTouchListener事件

	AddressService中“归属地提示框”是view,给view增加点击事件,即在showToast()方法中调用setTouch()方法,创建这个方法

	将DragViewActivity中setTouch方法中的代码拷贝过来,将ll_dragview_toast改为view,删除不需要的代码,这里将“3.计算移动的偏移量”留了下来,但是在showToast()方法中有一个params.x,代表的是距离边框的距离



	所以可将showToast()中的params设置成成员变量,在setTouch()的第四步“ 4.让控件移动相应的偏移量,重新绘制控件”写成params.x+=dx; params.y += dY;



	/**
  • 显示号码归属地提示框的触摸事件
     */
     private void setTouch() {
     view.setOnTouchListener(new OnTouchListener() {
     // 设置成成员变量是为了在移动的事件能够进行使用
     private int startX;
     private int startY;

      		// v : 控件
    
      		// event : 当前执行的事件
    
      		@Override
    
      		public boolean onTouch(View v, MotionEvent event) {
    
      			// 获取当前的事件
    
      			switch (event.getAction()) {
    
      			case MotionEvent.ACTION_DOWN:
    
      				// 按下的事件
    
      				System.out.println("按下了.....");
    
      				// 1.记录开始的x和y的坐标
    
      				startX = (int) event.getRawX();
    
      				startY = (int) event.getRawY();
    
      				break;
    
      			case MotionEvent.ACTION_MOVE:
    
      				// 移动的事件
    
      				System.out.println("移动了.....");
    
      				// 2.获取新的位置的x和y的坐标
    
      				int newX = (int) event.getRawX();
    
      				int newY = (int) event.getRawY();
    
      				// 3.计算移动的偏移量
    
      				int dX = newX - startX;
    
      				int dY = newY - startY;
    
      				// 4.让控件移动相应的偏移量,重新绘制控件
    
      				params.x += dX;
    
      				params.y += dY;
    
    
    
      				// 实现坐标和控件实际对应,防止控件坐标移出屏幕,如果params.x移出屏幕,那就设置params.x极限值
    
      				if (params.x < 0) {
    
      					params.x = 0;
    
      				}
    
      				if (params.x > width - view.getWidth()) {
    
      					params.x = width - view.getWidth();
    
      				}
    
      				if (params.y < 0) {
    
      					params.y  = 0;
    
      				}
    
      				if (params.y > height - view.getHeight() -25 ) {
    
      					params.y = height - view.getHeight() -25;
    
      				}
    
      				//更新控件
    
      				windowManager.updateViewLayout(view, params);
    
      				// 5.更新开始的坐标,为了让每次都是新的位置
    
      				startX = newX;
    
      				startY = newY;
    
      				break;
    
      			case MotionEvent.ACTION_UP:
    
      				// 抬起的事件
    
      				System.out.println("抬起了.....");
    
      				// 获取坐标
    
      				int x = params.x;// 获取移动过的控件的距离屏幕左边框的距离
    
      				int y = params.y;
    
      				// 保存控件的坐标,而不是手指的坐标
    
      				Editor edit = sp.edit();
    
      				edit.putInt("x", x);
    
      				edit.putInt("y", y);
    
      				edit.commit();
    
      				break;
    
      			}
    
      			// True if the listener has consumed the event, false otherwise.
    
      			// true:消费了,执行了,false:不执行
    
      			// 返回true表明我们down已经执行完了,可以执行移动和抬起的事件了
    
      			return true;
    
      		}
    
      	});
    
      }
    
    
    
      紧接着要重新绘制控件,这个view控件是在windowmanager中,windowmanager中有一个updateViewLayout(v,params)
    
      参1需要个view,参2需要个layoutparams,下边获取坐标这里,就可以将:
    
      	int x = ll_dragview_toast.getLeft();
    
      	int y = ll_dragview_toast.getTop();
    
      	改为int x = params.x; int y = params.y;
    
      	x,y代表的就是控件距离边框距离,getLeft也是控件距离边框距离,只有将onTouch事件返回值改为true,才表明down已执行完了,可以执行移动和抬起事件了(前边先开始是true,后来添加2击事件时改为了false,这里没有2击事件,所以又要改成true)
    
    
    
      运行程序,发现拨打电话界面的归属地提示框不能移动,这和setOnTouchListener没有关系了
    
      原因在上边showToast中的params中设置了吐司的属性flags为不可获取焦点,不可触摸
    
    
    
      先把这行不能触摸的代码注释掉
    
    
    
      运行程序,还是不能移动,把不可获取焦点也去掉还是不能移动,不是flags原因
    
    
    
      原因是params.type这里设置的TYPE_TOAST,也就是设置的吐司类型,吐司天生没有触摸事件,换个类型就可以
    
      改为优先于电话的类型TYPE_PRIORITY_PHONE,在这里写了优先于电话的级别,说明使用这种类型就可以在电话界面进行一些操作,比如设置控件
    
    
    
      TYPE_PRIORITY_PHONE : 优先于电话的类型
    
      用了优先于电话的类型必须添加一个权限:
    
      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    
    
    
      运行程序,拨打电话,可以移动了,但是当准备挂断电话时不可以挂断,原因是刚才将AddressService中的showToast中不可获取焦点这行代码给注释掉了,那我放开:
    
    
    
      	// LayoutParams:设置属性
    
      	params = new WindowManager.LayoutParams();
    
      	params.height = WindowManager.LayoutParams.WRAP_CONTENT;// 高度包裹内容
    
      	params.width = WindowManager.LayoutParams.WRAP_CONTENT;// 宽度包裹内容
    
      	params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE// 不可获取焦点
    
      			// | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE //不可触摸
    
      			| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; // 保持屏幕常亮
    
    
    
      运行程序,没有问题了,但在拨打电话界面中将控件一直往右移动(钻不到右边框里边),然后挂断电话,之后来到设置中心,发现设置中心中的控件找不到了(钻到右边框里边了),在来拨打电话,拨打电话界面中这个控件还在,然后将控件往左边移动,移动了好多次才移动过去
    
    
    
      原因:在AddressService中的ACTION_MOVE中,没有对是否移出屏幕进行判断,但是拨打电话界面不能让任何控件移出屏幕,也就是说既然在拨打电话界面没有对是否移出屏幕进行判断,那在拨打电话界面中一直往右边框外边移动控件,控件应该是要移动出屏幕的,但是因为拨打电话界面的特性,所以不能移出屏幕,也就是往右边框外移动控件,控件虽然还在屏幕中,但是控件的坐标已经到右边框外边了,这也就是在设置中心界面中,看不到控件原因
    
    
    
      在拨打电话界面中,控件移动到边框之后不能再让他坐标移动了,应该检测下,当控件移动到边框时,不应该让控件继续往外移动了
    
    
    
      比如控件往左边框处移动,如果控件左上角到左边框的距离小于0,就让这个距离永远等于0,那这样坐标和控件一直保持一致
    
    
    
      到AddressService中的setTouch中的ACTION_MOVE中,添加实现坐标和控件实际对应,防止控件坐标移出屏幕,如果params.x移出屏幕,那就设置params.x极限值
    
      
    
      	控件向左边框外移动时: 如果x值小于0,那就让x值为0
    
      	控件向右边框外移动时:如果x值大于屏幕宽度-控件宽度,那就让x值为屏幕宽度-控件宽度
    
      	控件向上边框移动时:如果y值小于0,那就让y值为0,
    
      	控件向下边框移动时:如果y值大于屏幕高度-控件高度-导航栏高度,那就让y值为屏幕高度-控件高度-导航栏高度
    
    
    
      // 实现坐标和控件实际对应,防止控件坐标移出屏幕,如果params.x移出屏幕,就设置params.x极限值
    
      if (params.x < 0) {
    
      	params.x = 0;
    
      }
    
      if (params.x > width - view.getWidth()) {
    
      	params.x = width - view.getWidth();
    
      }
    
      if (params.y < 0) {
    
      	params.y  = 0;
    
      }
    
      if (params.y > height - view.getHeight() -25 ) {
    
      	params.y = height - view.getHeight() -25;
    
      }
    
      
    
      运行程序,拨打电话界面中将控件向右边框外移动移不出去,挂断电话,来到设置中心发现控件还在,防止坐标移出屏幕操作就做成功了
    
    
    
      总结如下:
    
    
    
      1.在addressService中的setTouch方法中给view增加触摸事件,修改移动事件中的操作
    
      				
    
      				params.x += dX;
    
      				params.y += dY;
    
    
    
      				// 实现坐标和控件实际对应,防止控件坐标移出屏幕,如果params.x移出屏幕,那就设置params.x极限值
    
      				if (params.x < 0) {
    
      					params.x = 0;
    
      				}
    
      				if (params.x > width - view.getWidth()) {
    
      					params.x = width - view.getWidth();
    
      				}
    
      				if (params.y < 0) {
    
      					params.y  = 0;
    
      				}
    
      				if (params.y > height - view.getHeight() -25 ) {
    
      					params.y = height - view.getHeight() -25;
    
      				}
    
      				//更新控件
    
      				windowManager.updateViewLayout(view, params);
    
    
    
      2.在addressService中的showToast方法中修改控件的params属性设置,将flags中的不可触摸注释,同时修改type为TYPE_PRIORITY_PHONE,同时增加权限 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    
    
    
      	params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE// 不可获取焦点
    
      			// | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE //不可触摸
    
      			| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; // 保持屏幕常亮
    
      	params.format = PixelFormat.TRANSLUCENT; // 透明
    
      	params.type = WindowManager.LayoutParams.TYPE_PRIORITY_PHONE;// 控件的类型,显示窗体上的类型,toast天生没有触摸事件
    

6.13 小火箭 #

小火箭发射是一个触摸事件可直接拷贝,重要的是小火箭发射后下边的云出来了,火焰可以喷射

火焰是帧动画,小火箭发射后出来的云是渐变动画



创建项目“小火箭”

	它是透明背景,那来到清单文件中,将application的theme改为:

	android:theme="@android:style/Theme.Translucent.NoTitleBar"



	在activity_main中来个ImageView,找到小火箭图片放到drawable下,有两张,它两火焰不一样

	在ImageView中先简单设置一个desktop_recket_launch_1



		activity.main.xml

	

		<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

			xmlns:tools="http://schemas.android.com.tools"

			android:layout_width="match_parent"

			android:layout_height="match_parent">

		<ImageView

			android:id="@+id/iv_rocket"

			android:layout_width="wrap_content"

			android:layout_height="wrap_content"

			android:background="@drawable/desktop_recket_launch_1"

			android:layout_centerInParent="true"/>

		</RelativeLayout>



	MainActivity的onCreate中初始化控件:



		public class MainActivity extends Activity{

			private IamgeView iv_rocket;

			@Override

			protected void onCreate(Bundle savedInstanceState){

				super.onCreate(savedInstanceState);

				setContentView(R.layout.activity_main);

				iv_rocket = (ImageView) findViewById(R.id.iv_rocket);

			}

		}



	运行程序,小火箭显示出来了



	接下来给这个小火箭增加触摸事件,今天的重点都是触摸监听



	在onCreate中调用setTouch(),生成这个方法,将DragViewActivity中setTouch中的代码复制过来,将控件名ll_dragview_toast全部改为小火箭iv_rocket,“判断控件是否移出屏幕”这个代码就不需要了把它去掉,还有“绘制控件之后执行控件textview显示隐藏操作”这里也不需要,ACTION_UP中抬起的事件的所有代码都不要了,最后将返回值改为return true



	运行程序,小火箭可以移动了



	当松开手时,小火箭要发送出去,这个操作是在松开手时做的,所以在ACTION_UP中,调用sendRocket()方法,然后创建出这个方法,setRocket方法是用来发送小火箭的,发射小火箭时执行的是直接往上边走,那一直往上走,一直改变的是y坐标,y坐标是getTop(),也就是距离顶部的距离



	在sendRocket()方法中获取下getTop值

	既然改变的一直是这个值,要想实现这种平稳滑动效果,要一直去改变,往上走时这个坐标是从大到小,安卓中它的坐标是在左上角的那个点,也就是要一直从大到小改变这个距离,来个for循环循环个30次,代表要改变30次,每次让getTop获取的值-20



	这一步操作是在不断减小距离顶部距离

	一直改变这个y值,小火箭也应该要能一直显示出来,所以要重新绘制这个控件,用的还是measure(),layout(),drawable()三个方法中的layout(l,t,r,b)方法

		参数l = iv_rocket.getLeft()

		参数t的值 就是我们的top

		参数r的值 就是l的值加上控件的宽度: iv_rocket.getLeft()_iv+iv_rocket.getWidth()

		参数b的值 就是top+控件的高度iv_rocket.getHeight()



    运行程序,发射小火箭,小火箭直接不见了,原因是在改变时一直没有让它停顿,一次性直接执行完了,在安卓中,这个30次,根本用不了100毫秒就执行完了,所以应该让他每次循环的时候,都睡上个100毫秒



	既然让他睡,应该在子线程中执行,不能让主线程睡

	在这里new一个Thread,将睡100毫秒操作放到run中执行,每次循环都让它在子线程中睡上几秒钟

	重新绘制控件,一直在更改UI,不能在子线程中更改主线程ui,有个runOnUiThread(action)方法,在它的run方法中执行“重新绘制控件”的操作,这个操作就相当于运行在主线



	点击runOnUiThread查看源码,里边就是用了一个handler



	运行程序,小火箭发射后不会瞬间不见了,但还是太快,把每次减的数值由20调到15,再运行程序,但还是太快,将睡眠值由100改为500,发现小火箭发射后,尾巴还漏在上部



	不是睡眠原因,原因是在这里边绘制了一个控件,每次都让它减小一个值,每次减小这个值,然后才绘制这个控件,所以将减小这个值的操作放到子线程中执行看下



	运行程序,发现小火箭都不能发射了,所以也不是这个原因,那就将减小这个值的代码再放回主线程中



	将循环的30改为60,运行程序,再来看下,发现也不是这个问题



	注意看上边代码是循环了30次,每次都new了一个子线程,执行操作时应该是在线程当中执行逐渐减少操作的,所以应该将for循环放到子线程中执行:



		 /**
  • 发射小火箭
     */
     prortected void sendRocket(){

      		//执行操作的时候是在线程当中执行逐渐减少操作的
    
      
    
      			new Thread(){
    
      				public void run(){
    
      					for(int i =0; i < 30; i++){
    
      					SystemClock.sleep(100);
    
      					//运行在主线程,内部其实就是封装了一个handler
    
      					runOnUiThread(new Runnable(){
    
      						@Override
    
      						public void run(){
    
      							int top = iv_rocket.getTop()-20;
    
      							//重新绘制控件
    
      							iv_rocket.layout(iv_rocket.getLeft(),top,iv_rocket.getLeft()+iv_rocket.getWidth(),top+iv_rocket.getHeight());
    
      						}
    
      					});
    
      				}
    
      			  };
    
      			}.start();
    
      		}
    
      	}
    
    
    
      每循环一次,让它睡100毫秒再执行代码
    
    
    
      运行程序,小火箭发射又太慢了,让它睡10毫秒,这次发现差不多了,但是在任何地方都可以发射这个小火箭,效果是要小火箭在底部中间的位置才可以发射
    
    
    
      MainActivity中的setTouch方法中的ACTION_UP中,发射时要做个判断,判断是否是底部中间的位置
    
    
    
      先判断是不是底部,iv_rocket.getTop()是距离顶部的距离,设置如果大于290,并且还要判断是否是中间位置,中间位置怎么判断?
    
      可以拿一个距离左边框的值,如果大于100并且小于200的时候,就发射小火箭,即:
    
    
    
      	case MotionEvent.ACTION_UP:
    
      		// 抬起的事件
    
      		//判断是否是底部中间的位置
    
      		if (iv_rocket.getTop() > 290  && iv_rocket.getLeft() > 100 && iv_rocket.getLeft() < 200) {
    
      			sendRocket();
    
      		}
    
      		break;
    
    
    
      运行程序,将小火箭移动到左边,右边,中间偏上,都没有发送,只有移动到下边的中间才发射了,但是发现顶部还有个火焰的尾巴
    
      说明循环的次数还不够,来个40次就没有问题了:
    
    
    
      	/**
    
  • 发射小火箭
     */
     prortected void sendRocket(){

      		//执行操作的时候是在线程当中执行逐渐减少操作的
    
      
    
      			new Thread(){
    
      				public void run(){
    
      					for(int i =0; i < 40; i++){
    
      					SystemClock.sleep(100);
    
      					//运行在主线程,内部其实就是封装了一个handler
    
      					runOnUiThread(new Runnable(){
    
      						@Override
    
      						public void run(){
    
      							int top = iv_rocket.getTop()-20;
    
      							//重新绘制控件
    
      							iv_rocket.layout(iv_rocket.getLeft(),top,iv_rocket.getLeft()+iv_rocket.getWidth(),top+iv_rocket.getHeight());
    
      						}
    
      					});
    
      				}
    
      			  };
    
      			}.start();
    
      		}
    
      	}
    
    
    
      发射效果实现了,还有个火焰喷射效果,火焰喷射效果就是帧动画,帧动画前边讲过
    
    
    
      看下api文档中,找到adt-bundle-windwo-x86_64_20140101/sdk/docs/index.html,API Guides/Animation and Graphics/点击帧动画Drawable Animation,它用英文说在res/drawable下,创建了一个xml文件,把这个动画放在drawable下没问题,但一般会新建一个anim文件夹,将动画放到这个文件夹下,在anim文件夹下创建rocket.xml动画,将api中的那段代码拷贝过来:
    
    
    
      	rocket.xml
    
      
    
      	<?xml version="1.0" encoding="utf-8"?>
    
      	<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    
      		android:oneshot="false"> <!--onshot: 是否执行一次 true:执行 false:循环执行 -->
    
      		<item android:drawable="@drawable/rocket_thrust1" android:durtion="200"/>
    
      		<item android:drawable="@drawable/rocket_thrust2" android:durtion="200"/>
    
      		<item android:drawable="@drawable/rocket_thrust3" android:durtion="200"/>
    
      	</animation-list>
    
    
    
      onshot: 是否执行一次 true:执行一次 false:循环执行,我们改成false
    
      这里只有两张图片,所以去掉一个item,并将我们的图片名替换掉rocket_thrust,即:
    
    
    
      rocket.xml
    
    
    
      <?xml version="1.0" encoding="utf-8"?>
    
      <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    
      	android:oneshot="true"> <!--onshot: 是否执行一次 true:执行一次 false:循环执行 -->
    
      	<item android:drawable="@drawable/desktop_rocket_launch_1" android:durtion="200"/>
    
      	<item android:drawable="@drawable/desktop_rocket_launch_2" android:durtion="200"/>
    
      </animation-list>
    
    
    
      动画的xml有了,再看api,给一个imageView设置了setBackgroundResource,这个是用代码设置的
    
    
    
      用布局文件形式设置动画,到activity_main.xml中给ImageView添加:
    
      android:background="@anim/rocket"
    
    
    
      activity.main.xml
    
    
    
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    
      	xmlns:tools="http://schemas.android.com.tools"
    
      	android:layout_width="match_parent"
    
      	android:layout_height="match_parent">
    
      <ImageView
    
      	android:id="@+id/iv_rocket"
    
      	android:layout_width="wrap_content"
    
      	android:layout_height="wrap_content"
    
      	android:layout_centerInParent="true"
    
      	android:background="@anim/rocket"/>
    
      </RelativeLayout>
    
    
    
      用布局文件形式设置动画已完成,api文档是用iamgeView获取getBackground()得到AnimationDrawable
    
    
    
      MainActivity的onCreate中,用iv_rocket调用getBackground(),返回AnimationDrawable,把它设置成成员变量:
    
    
    
      	public class MainActivity extends Activity{
    
      		private IamgeView iv_rocket;
    
      		private AnimationDrawable animationDrawable;
    
      		@Override
    
      		protected void onCreate(Bundle savedInstanceState){
    
      			super.onCreate(savedInstanceState);
    
      			setContentView(R.layout.activity_main);
    
      			iv_rocket = (ImageView) findViewById(R.id.iv_rocket);
    
      			animationDrawable = (AnimationDrawable) iv_rocket.getBackground();
    
      		}
    
      	}
    
    
    
      api文档执行了start(),到MainActivity的setTouch的onTouch中,当触摸之后,开启动画:
    
      	@Override
    
      	public boolean onTouch(View v,MotionEvent event){
    
      		//开启动画
    
      		animationDrawable.start();//在低版本中不太兼容
    
      	}
    

    运行程序,只要触摸小火箭,动画就一直执行,效果一直在喷射火焰,就是两张图片在一直变化,这个动画在低版本上不太兼容,看不到火焰效果

    小火箭喷射效果有了,还有云的效果,找到云的两个图片,放到的drawable-hdpi文件夹下,它这个烟雾在底部实现的

    在activity_main.xml中再来两个ImageView:

      activity.main.xml
    
    
    
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    
      	xmlns:tools="http://schemas.android.com.tools"
    
      	android:layout_width="match_parent"
    
      	android:layout_height="match_parent"
    
      	android:background="#000000">
    
      <ImageView
    
      	android:id="@+id/iv_rocket"
    
      	android:layout_width="wrap_content"
    
      	android:layout_height="wrap_content"
    
      	android:layout_centerInParent="true"
    
      	android:background="@anim/rocket"/>
    
      <ImageView
    
      	android:id="@+id/iv_rocket_m"
    
      	android:layout_width="wrap_content"
    
      	android:layout_height="wrap_content"
    
      	android:src="@drawable/desktop_smoke_m"
    
      	android:layout_alignParentBottom="true"
    
      	android:visibility="invisible"/>
    
      <ImageView
    
      	android:id="@+id/iv_rocket_t"
    
      	android:layout_width="wrap_content"
    
      	android:layout_height="wrap_content"
    
      	android:src="@drawable/desktop_smoke_t"
    
      	android:layout_marginTop="155"
    
      	android:visibility="invisible"/>
    
      </RelativeLayout>
    

    MainActivity的onCreate中初始化它们:

      public class MainActivity extends Activity{
    
      	private IamgeView iv_rocket;
    
      	private ImageView iv_rocket_t;
    
      	private ImageView iv_rocket_m;
    
      	private AnimationDrawable animationDrawable;
    
      	@Override
    
      	protected void onCreate(Bundle savedInstanceState){
    
      		super.onCreate(savedInstanceState);
    
      		setContentView(R.layout.activity_main);
    
      		iv_rocket = (ImageView) findViewById(R.id.iv_rocket);
    
      		iv_rocket_t = (ImageView) findViewById(R.id.iv_rocket_t);
    
      		iv_rocket_m = (ImageView) findViewById(R.id.iv_rocket_m);
    
      		animationDrawable = (AnimationDrawable) iv_rocket.getBackground();
    
    
    
      		setTouch();
    
      	}
    
      }
    

    接下来实现渐变动画,创建AlphaAnimation(fromAlpha,toAlpha),从0到1:

      public class MainActivity extends Activity{
    
      	private IamgeView iv_rocket;
    
      	private ImageView iv_rocket_t;
    
      	private ImageView iv_rocket_m;
    
      	private AnimationDrawable animationDrawable;
    
      	@Override
    
      	protected void onCreate(Bundle savedInstanceState){
    
      		super.onCreate(savedInstanceState);
    
      		setContentView(R.layout.activity_main);
    
      		iv_rocket = (ImageView) findViewById(R.id.iv_rocket);
    
      		iv_rocket_t = (ImageView) findViewById(R.id.iv_rocket_t);
    
      		iv_rocket_m = (ImageView) findViewById(R.id.iv_rocket_m);
    
      		animationDrawable = (AnimationDrawable) iv_rocket.getBackground();
    
    
    
      		AhphaAnimation alphaAnimation = new AlphaAnimation(0,1);//从透明到不透明
    
    
    
      		setTouch();
    
      		}
    
      }
    

    意思是从透明到不透明,还可以设置一个属性setDuration(durationMillis),持续时间设置成2秒:

      public class MainActivity extends Activity{
    
    
    
      	private IamgeView iv_rocket;
    
      	private ImageView iv_rocket_t;
    
      	private ImageView iv_rocket_m;
    
      	private AnimationDrawable animationDrawable;
    
      	@Override
    
      	protected void onCreate(Bundle savedInstanceState){
    
      		super.onCreate(savedInstanceState);
    
      		setContentView(R.layout.activity_main);
    
      		iv_rocket = (ImageView) findViewById(R.id.iv_rocket);
    
      		iv_rocket_t = (ImageView) findViewById(R.id.iv_rocket_t);
    
      		iv_rocket_m = (ImageView) findViewById(R.id.iv_rocket_m);
    
      		animationDrawable = (AnimationDrawable) iv_rocket.getBackground();
    
    
    
      		AhphaAnimation alphaAnimation = new AlphaAnimation(0,1);//从透明到不透明
    
      		alphaAnimation.setDuration(2000);
    
      		setTouch();
    
           }
    
       }
    

    渐变动画有了,发射时才会显示出来

    来到ACTION_U中“判断是否是底部中间的位置”中来startAnimation(animation):

      case MotionEvent.ACTION_UP:
    
      		// 抬起的事件
    
      		//判断是否是底部中间的位置
    
      		if (iv_rocket.getTop() > 290  && iv_rocket.getLeft() > 100 && iv_rocket.getLeft() < 200) {
    
      			sendRocket();
    
      			iv_rocket_m.startAnimation(alphaAnimation);//开始执行动画
    
      			iv_rocket_t.startAnimation(alphaAnimation);
    
      		}
    
      		break;
    

    发射完成后云的动画应该消失掉,在sendRocket()方法中一直在改变top值,在里边再来个runOnUiThread,循环完后,让云动画消除掉:

      /**
    
  • 发射小火箭
     */
     prortected void sendRocket(){

      		//执行操作的时候是在线程当中执行逐渐减少操作的
    
      		new Thread(){
    
      			public void run(){
    
      				for(int i =0; i < 40; i++){
    
      				SystemClock.sleep(100);
    
      				//运行在主线程,内部其实就是封装了一个handler
    
      				runOnUiThread(new Runnable(){
    
      					@Override
    
      					public void run(){
    
      						int top = iv_rocket.getTop()-20;
    
      						//重新绘制控件
    
      						iv_rocket.layout(iv_rocket.getLeft(),top,iv_rocket.getLeft()+iv_rocket.getWidth(),top+iv_rocket.getHeight());
    
      					}
    
      				});
    
      			}
    
      			runOnUiThread(new Runnable(){
    
      				@Override
    
      				public void run(){
    
      					iv_rocket_t.clearAnimation();//消除动画
    
      					iv_rocket_m.clearAnimation();
    
      				}
    
      			});
    
      		  };
    
      		}.start();
    
      	}
    
      }
    
    
    
      运行程序,点击发送,就有个云出来了,发送完,云就消失了
    
    
    
      以上就是小火箭全部操作,最重要就是setOnTouchListener方法,这个必须得会,其次就是帧动画animationDrawable,再其次就是渐变动画AlphaAnimation
    
    
    
      步骤总结如下:
    
    
    
    
    
      1.触摸事件
    
    
    
      /**
    
       *设置触摸监听
    
       */
    
      private void setTouch(){
    
      	iv_rocket.setOnTouchListener(new OnTouchListener() {
    
      		// 设置成成员变量是为了在移动的事件能够进行使用
    
      		private int startX;
    
      		private int startY;
    
    
    
      		// v : 控件
    
      		// event : 当前执行的事件
    
      		@Override
    
      		public boolean onTouch(View v, MotionEvent event) {
    
      			//开启动画
    
      			animationDrawable.start();//在低版本中不太兼容
    
      			// 获取当前的事件
    
      			switch (event.getAction()) {
    
      			case MotionEvent.ACTION_DOWN:
    
      				// 按下的事件
    
      				System.out.println("按下了.....");
    
      				// 1.记录开始的x和y的坐标
    
      				startX = (int) event.getRawX();
    
      				startY = (int) event.getRawY();
    
      				break;
    
      			case MotionEvent.ACTION_MOVE:
    
      				// 移动的事件
    
      				System.out.println("移动了.....");
    
      				// 2.获取新的位置的x和y的坐标
    
      				int newX = (int) event.getRawX();
    
      				int newY = (int) event.getRawY();
    
      				// 3.计算移动的偏移量
    
      				int dX = newX - startX;
    
      				int dY = newY - startY;
    
      				// 4.让控件移动相应的偏移量,重新绘制控件
    
      				int l = iv_rocket.getLeft();// 获取控件距离父控件左边框的距离,原控件(没移动前)的距离
    
      				int t = iv_rocket.getTop();// 获取控件距离父控件顶部的距离
    
      				// 移动相应的偏移量
    
      				l += dX;
    
      				t += dY;
    
      				int r = l + iv_rocket.getWidth();
    
      				int b = t + iv_rocket.getHeight();
    
      				// r:表示控件右边框距离屏幕左边框的距离
    
      				iv_rocket.layout(l, t, r, b);// 重新绘制控件
    
    
    
      				// 5.更新开始的坐标,为了让每次都是新的位置
    
      				startX = newX;
    
      				startY = newY;
    
      				break;
    
      			case MotionEvent.ACTION_UP:
    
      				// 抬起的事件
    
      				//判断是否是底部中间的位置
    
      				if (iv_rocket.getTop() > 290  && iv_rocket.getLeft() > 100 && iv_rocket.getLeft() < 200) {
    
      					sendRocket();
    
      					iv_rocket_m.startAnimation(alphaAnimation);//开始执行动画
    
      					iv_rocket_t.startAnimation(alphaAnimation);
    
      				}
    
      				break;
    
      			}
    
      			// True if the listener has consumed the event, false otherwise.
    
      			// true:消费了,执行了,false:不执行
    
      			// 返回true表明我们down已经执行完了,可以执行移动和抬起的事件了
    
      			return true;
    
      		}
    
      	});
    
      }		
    
    
    
    
    
      2.发送小火箭
    
    
    
      	/**
    
  • 发射小火箭
     */
     protected void sendRocket() {
     // 执行操作的时候是在线程当中执行逐渐减少操作的

      		new Thread() {
    
      			public void run() {
    
      				for (int i = 0; i < 40; i++) {
    
      					SystemClock.sleep(10);
    
      					// 运行在主线程,内部封装了一个handler
    
      					runOnUiThread(new Runnable() {
    
      						@Override
    
      						public void run() {
    
      							int top = iv_rocket.getTop() - 15;// 是在不断的减小距离顶部的距离
    
      							// 重新绘制控件
    
      							iv_rocket.layout(iv_rocket.getLeft(), top,
    
      									iv_rocket.getLeft() + iv_rocket.getWidth(),
    
      									top + iv_rocket.getHeight());
    
      						}
    
      					});
    
      				}
    
      				runOnUiThread(new Runnable() {
    
      					@Override
    
      					public void run() {
    
      						iv_rocket_t.clearAnimation();//消除动画
    
      						iv_rocket_m.clearAnimation();
    
      					}
    
      				});
    
      			};
    
      		}.start();
    
      	}
    
    
    
      3.帧动画和渐变动画
    
    
    
      	帧动画
    
    
    
      		1.res -> anim -> xxx.xml
    
      		<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    
      		    android:oneshot="false"><!-- oneshot : 是否执行一次   true:执行一次  false:循环执行 -->
    
      		    <item android:drawable="@drawable/desktop_rocket_launch_1" android:duration="200" />
    
      		    <item android:drawable="@drawable/desktop_rocket_launch_2" android:duration="200" />
    
      		</animation-list>
    
      		2.控件使用
    
      		android:background="@anim/rocket"
    
      		3.代码中使用
    
      		animationDrawable = (AnimationDrawable) iv_rocket.getBackground();
    
      		//开启动画
    
      		animationDrawable.start();//在低版本中不太兼容
    
    
    
      	渐变动画
    
      		1.创建一个渐变动画
    
      			alphaAnimation = new AlphaAnimation(0, 1);
    
      			alphaAnimation.setDuration(2000);
    
      		2.开启动画
    
      			iv_rocket_m.startAnimation(alphaAnimation);//开始执行动画
    
      		3.清除动画
    
      			iv_rocket_t.clearAnimation();//消除动画
    

6.14 总结 #

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值