GVKun编程网logo

教你一步步实现Android微信自动抢红包(教你一步步实现android微信自动抢红包功能)

25

本文将介绍教你一步步实现Android微信自动抢红包的详细情况,特别是关于教你一步步实现android微信自动抢红包功能的相关信息。我们将通过案例分析、数据研究等多种方式,帮助您更全面地了解这个主题,

本文将介绍教你一步步实现Android微信自动抢红包的详细情况,特别是关于教你一步步实现android微信自动抢红包功能的相关信息。我们将通过案例分析、数据研究等多种方式,帮助您更全面地了解这个主题,同时也将涉及一些关于Androd强化抢红包实现不了自动抢红包、Android Studio一步步教你集成发布适配、Android 中微信抢红包助手的实现、Android 视图绘制流程完全解析,带你一步步深入了解 View (二)的知识。

本文目录一览:

教你一步步实现Android微信自动抢红包(教你一步步实现android微信自动抢红包功能)

教你一步步实现Android微信自动抢红包(教你一步步实现android微信自动抢红包功能)

本文介绍微信自动抢红包的实现方法,主要实现以下几个功能:

      1.自动拆开屏幕上出现的红包

      2.处于桌面或聊天列表时接收到红包信息时自动进入聊天界面并拆红包

      3.日志功能,记录抢红包的详细日志

实现原理

     1.利用AccessibilityService辅助服务,监测屏幕内容,实现自动拆红包的目的。

     2.利用ActiveAndroid数据库简单记录红包日志

     3.利用preference实现监控选项纪录

最终界面

抢红包核心代码

AccessibilityService配置

android:accessibilityEventTypes 设置触发监听回调的事件类型;

android:packageNames 设置监听的应用,这里监听的是微信,因此填上微信的包名com.tencent.mm

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged"
 android:accessibilityFeedbackType="FeedbackGeneric"
 android:accessibilityFlags="flagDefault"
 android:canRetrieveWindowContent="true"
 android:description="@string/accessibility_description"
 android:notificationTimeout="100"
 android:packageNames="com.tencent.mm"
 android:settingsActivity="com.oden.annotations.app.activity.ManActivity" />

在AndroidManifest.xml中声明:

 <service
   android:name=".app.service.HongbaoService_"
   android:enabled="true"
   android:exported="true"
   android:label="@string/app_name"
   android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" >
   <intent-filter>
    <action android:name="android.accessibilityservice.AccessibilityService" />
   </intent-filter>
   <Meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility_service_config" />
  </service>

抢红包实现代码

接收系统发送来的AccessibilityEvent

 private static final String GET_RED_PACKET = "领取红包";
 private static final String CHECK_RED_PACKET = "查看红包";
 private static final String RED_PACKET_PICKED = "手慢了,红包派完了";
 private static final String RED_PACKET_PICKED2 = "手气";
 private static final String RED_PACKET_PICKED_DETAIL = "红包详情";
 private static final String RED_PACKET_SAVE = "已存入零钱";
 private static final String RED_PACKET_NOTIFICATION = "[微信红包]";

 @Override
 public void onAccessibilityEvent(AccessibilityEvent event) {
  L.d("RECEIVE EVENT!");
  if (watchedFlags == null) return;
   /* 检测通知消息 */
  if (!mMutex) {
   if (watchedFlags.get("pref_watch_notification") && watchNotifications(event)) return;
   if (watchedFlags.get("pref_watch_list") && watchList(event)) return;
  }
  if (!watchedFlags.get("pref_watch_chat")) return;

  this.rootNodeInfo = event.getSource();
  if (rootNodeInfo == null) return;

  mReceiveNode = null;
  mUnpackNode = null;

  checkNodeInfo();

   /* 如果已经接收到红包并且还没有戳开 */
  if (mLuckyMoneyReceived && !mLuckyMoneyPicked && (mReceiveNode != null)) {
   mMutex = true;
   AccessibilityNodeInfo cellNode = mReceiveNode;
   cellNode.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
   mLuckyMoneyReceived = false;
   mLuckyMoneyPicked = true;
   L.d("正在打开!");
  }

   /* 如果戳开但还未领取 */
  if (mNeedUnpack && (mUnpackNode != null)) {
   AccessibilityNodeInfo cellNode = mUnpackNode;
   cellNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
   mNeedUnpack = false;
   L.d("正在领取!");
  }

  if (mNeedBack) {
   performGlobalAction(GLOBAL_ACTION_BACK);
   mMutex = false;
   mNeedBack = false;
   L.d("正在返回!");
   //总次数和金额统计
   if (isGetMoney) {
    T.showShort(this,"抢到一个红包: " + gotMoney + "元!");
    totalMoney = totalMoney + gotMoney;
    totalSuccessNum++;
    myPrefs.totalMoney().put(totalMoney);
    myPrefs.successNum().put(totalSuccessNum);
    L.d("totalMoney: " + totalMoney);
    L.d("totalSuccessNum: " + totalSuccessNum);
    savetoLog(hongbaoInfo);
    isGetMoney = false;
   }
  }
 }

检测监听事件的节点信息

private void checkNodeInfo() {
  L.d("checkNodeInfo!");
  if (this.rootNodeInfo == null) return;
   /* 聊天会话窗口,遍历节点匹配“领取红包”和"查看红包" */
  List<AccessibilityNodeInfo> nodes1 = this.findAccessibilityNodeInfosByTexts(this.rootNodeInfo,new String[]{
    GET_RED_PACKET,CHECK_RED_PACKET});
  if (!nodes1.isEmpty()) {
  L.d("!nodes1.isEmpty()");
   AccessibilityNodeInfo targetNode = nodes1.get(nodes1.size() - 1);
   if ("android.widget.LinearLayout".equals(targetNode.getParent().getClassName()))//避免被文字干扰导致外挂失效
   {
    if (this.signature.generateSignature(targetNode)) {
     mLuckyMoneyReceived = true;
     mReceiveNode = targetNode;
     L.d("signature:" + this.signature.toString());
    }
   } else {
    L.d("this is text");
   }
   return;
  }

  List<AccessibilityNodeInfo> nodes2 = this.findAccessibilityNodeInfosByTexts(this.rootNodeInfo,new String[]{
    "拆红包"});
  if (!nodes2.isEmpty()) {
   L.d("node2 != null");
   for (AccessibilityNodeInfo nodeInfo : nodes2) {
     if (nodeInfo.getClassName().equals("android.widget.Button"))
      nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
   }
  } else {
    /* 戳开红包,红包还没抢完,遍历节点匹配“拆红包” */
   AccessibilityNodeInfo node2 = (this.rootNodeInfo.getChildCount() > 3) ? this.rootNodeInfo.getChild(3) : null;
   if (node2 != null && node2.getClassName().equals("android.widget.Button")) {
    mUnpackNode = node2;
    mNeedUnpack = true;
    isToGetMoney = true;
    L.d("find red packet!");
    return;
   }
  }
   /* 戳开红包,红包已被抢完,遍历节点匹配“已存入零钱”和“手慢了” */
  if (mLuckyMoneyPicked) {
   List<AccessibilityNodeInfo> nodes3 = this.findAccessibilityNodeInfosByTexts(this.rootNodeInfo,new String[]{
     RED_PACKET_PICKED,RED_PACKET_SAVE,RED_PACKET_PICKED2,RED_PACKET_PICKED_DETAIL});
   if (!nodes3.isEmpty()) {
    L.d("!nodes3.isEmpty()");    
    if (rootNodeInfo.getChildCount() > 1) {
     L.d("RED_PACKET_PICKED!");
    } else {
     L.d("nodes3.get(0).toString(): " + nodes3.get(0).getText().toString());
     if (!nodes3.get(0).getText().toString().equals(RED_PACKET_PICKED_DETAIL)) {
      AccessibilityNodeInfo targetNode = nodes3.get(nodes3.size() - 1);
      hongbaoInfo.getInfo(targetNode);
      if (isToGetMoney) {
       isGetMoney = true;
       isToGetMoney = false;
       gotMoney = hongbaoInfo.getMoney();
       L.d("gotMoney: " + gotMoney);
      }
      L.d("RED_PACKET_SAVE!");
      L.d("hongbaoInfo: " + hongbaoInfo.toString());
     } else {
      L.d("this packet is myself!");
     }

    }
    mNeedBack = true;
    mLuckyMoneyPicked = false;
   }
  }
 }

主要通过检测“领取红包”等关键文字信息来判断是否有新红包

检测收到红包时判断是否"android.widget.LinearLayout",屏蔽聊天信息中的文字干扰

拆红包时,由于微信版本可能不同,同时进行两种判断,以兼容部分版本

拆完红包需自动返回,有以下几种情况:抢到了,手慢了,以及该红包是自己发出的红包

下面是监听聊天列表的代码:

private boolean watchList(AccessibilityEvent event) {
  // Not a message
  if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || event.getSource() == null)
   return false;

  List<AccessibilityNodeInfo> nodes = event.getSource().findAccessibilityNodeInfosByText(RED_PACKET_NOTIFICATION);
  if (!nodes.isEmpty()) {
   AccessibilityNodeInfo nodetoClick = nodes.get(0);
   CharSequence contentDescription = nodetoClick.getContentDescription();
   if (contentDescription != null && !lastContentDescription.equals(contentDescription)) {
    nodetoClick.performAction(AccessibilityNodeInfo.ACTION_CLICK);
    lastContentDescription = contentDescription.toString();
    return true;
   }
  }
  return false;
 }

下面是监听通知信息的代码:

 private boolean watchNotifications(AccessibilityEvent event) {
  // Not a notification
  if (event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)
   return false;

  // Not a hongbao
  String tip = event.getText().toString();
  if (!tip.contains(RED_PACKET_NOTIFICATION)) return true;

  Parcelable parcelable = event.getParcelableData();
  if (parcelable instanceof Notification) {
   Notification notification = (Notification) parcelable;
   try {
    notification.contentIntent.send();
   } catch (PendingIntent.CanceledException e) {
    e.printstacktrace();
   }
  }
  return true;
 }

红包信息的获取,及日志的存储

通过获取节点的子信息,分别获得红包发送者及抢到的金额、抢红包时间等信息,建立简单的表单分别记录该信息。

@Table(name = "HongbaoInfos")
public class HongbaoInfo extends Model {

 private int month;
 private int day;
 private int hour;
 private int min;
 private int sec;

 @Column(name = "sender")
 public String sender;

 @Column(name = "money")
 public String money;

 @Column(name = "time")
 public String time;

 public void getInfo(AccessibilityNodeInfo node) {

  AccessibilityNodeInfo hongbaoNode = node.getParent();
  sender = hongbaoNode.getChild(0).getText().toString();
  money = hongbaoNode.getChild(2).getText().toString();
  time = getStringTime();
 }

 private String getStringTime() {
  Calendar c = Calendar.getInstance();
  month = c.get(Calendar.MONTH) + 1;
  day = c.get(Calendar.DAY_OF_MONTH);
  hour = c.get(Calendar.HOUR_OF_DAY);
  min = c.get(Calendar.MINUTE);
  sec = c.get(Calendar.SECOND);
  return month+"月"+day+"日 "+hour+":"+min+":"+sec;
 }

 @Override
 public String toString() {
  return "HongbaoInfo [sender=" + sender + ",money=" + money + ",time=" + time + "]";
 }

 public static List<HongbaoInfo> getAll() {
  return new Select()
    .from(HongbaoInfo.class)
    .orderBy("Id ASC")
    .execute();
 }

 public static void deletealL() {
  new Delete().from(HongbaoInfo.class).execute();
 }

 public float getMoney() {
  return Float.parseFloat(money);
 }

 public String getSender() {
  return sender;
 }

 public String getTime() {
  return time;
 }
}

存储操作:

 private void savetoLog(HongbaoInfo hongbaoInfo) {
  if (watchedFlags.get("pref_etc_log")) {
   HongbaoInfo hongbaoInfo1 = new HongbaoInfo();
   hongbaoInfo1 = hongbaoInfo;
   hongbaoInfo1.save();
  } else {
   L.d("log closed!");
  }
 }

总结

主要的代码到这里基本结束,目前在微信最新版上测试ok,尚还存在以下几个问题:

    1.同一个人连续发的不能自动抢,因为为了防止重复点击做了过滤,同一个人的红包抢了后不会再次点击

    2.AccessibilityService开启时间长后有时会被系统关掉

结束语

以上就是本文的全部内容了,希望对大家的学习和工作能有所帮助。

Androd强化抢红包实现不了自动抢红包

Androd强化抢红包实现不了自动抢红包

List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("拆红包"); Log.i(TAG, "list:" + list.size()+""); for(AccessibilityNodeInfo n : list) {
    Log.i(TAG, "n:" + n);  n.performAction(AccessibilityNodeInfo.ACTION_CLICK); }

Android Studio一步步教你集成发布适配

Android Studio一步步教你集成发布适配

  • 开门见山,本章教你如何配置多渠道一键打包,本教程只符合使用Android Studio的童鞋
    1.首先检查本地gradle版本是否是最新的,我建议换成最新的编译版本gradle版本查看

我用的是gradle-2.10-all

用迅雷下载更快
https://downloads.gradle.org/distributions/gradle-2.10-all.zip
下载其它版本把“2.10”替换成你所需要的版本号就ok啦

点击可下载
下载后解压到任意你所指定的文件夹
2.最后给你的工程指定gradle版本位置,使用本地的,如图所示

gradle.png
3.我相信java环境都已经配置好了,所以无需再提。本教程并没有去木有配置gradle的环境变量,因为--没用到,至于gradle环境变量配置教程,有需要的童鞋可以看一下

环境变量配置错误,出现问题找我,我也不会认的0 - 0因为本教程,木有去配置gradle环境,木有去配置gradle环境,重要事情说三遍

(gradle环境变量配置)

WIN下环境变量添加方法:
我的电脑→属性→高级系统属性设置→高级→环境变量,先新建一个用户变量
GRADLE_HOME填入你上面解压知道的文件夹路径,之后在系统PATH变量下添
加GRADLE_HOME/bin;确定保存即可,重启命令行,然后运行
gradle -version查看当前gradle的版本号. 
MAC下添加环境变量:
$ vim .bash_profile 
export GRADLE_HOME=/Applications/gradle-2.3;
export PATH=$PATH:$GRADLE_HOME/bin
检查是否成功
$ gradle -version
  • 开始我们的配置
    1.首先是友盟渠道,相信很多公司产品都融入了这个

把UMENG_CHANNEL下面的值改为${UMENG_CHANNEL_VALUE

<meta-data    
   android:name="UMENG_CHANNEL" 
   android:value="${UMENG_CHANNEL_VALUE}" />

然后打开app目录下的build.gradle文件配置渠道

productFlavors {
        abc {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "abc"]
        }
       baiduyun {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baiduyun"]
        }
      wandoujia {
           manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
      }
    }

2.多渠道配置结束,就是这么简单,然后我们打包的时候肯定都希望能够区分每一个包是哪个渠道的,添加下面的代码可以实现

buildTypes {
        release {
            minifyEnabled true//是否混淆
            zipAlignEnabled true
            shrinkResources true//移除未使用的资源文件
            proguardFiles getDefaultProguardFile(''proguard-android.txt''), ''proguard-rules.pro''
     applicationVariants.all { variant ->
        variant.outputs.each { output ->
            if (output.outputFile != null && output.outputFile.name.endsWith(''.apk'')) {
              File outputDirectory = new File(outputFile.parent);
              def fileName
              if (variant.buildType.name == "release") {
                  fileName = "appName_v${defaultConfig.versionName}_${packageTime()}_${variant.productFlavors[0].name}.apk"
              } else {
                  fileName = "appName_v${defaultConfig.versionName}_${packageTime()}_beta.apk"}output.outputFile = new File(outputDirectory, fileName)
            }
        }
    }
}
}

然后点击 Sync Now
然后报错了是吧?恩,packageTime()这个方法都还没给你,放在顶部即可

def packageTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}

3.Sync Now然后按照下面步骤打包吧

 Build->Clean Project
    Build->Rebuild Project
    Build->Generate Singned APK...
    ->next配置你的签名配置->next 解释在下图
    ->finish

generater singned apk.png
完了就会在文件夹看到你需要的包了

apk.png

  • 自定义配置文件
    1.多个自动升级sdk的时候

比如想把包放入360和百度的时候,呵呵,大家都懂的,360是最近才开始必须接入他的升级sdk才能上架的。这里就不吐槽了,公司要求要去上架这些应用商城。
打一次包就能搞定这些接入问题,因为我一开始接入的是友盟自动升级,需求的增加就越来越多,ok开始配置:
先来做个假设:假设上面的abc包是360升级的,wandoujia是友盟升级的,baiduyun是百度升级的
定义一个名字为AUTO_TYPE的String类型常量 默认值为umeng放在defaultConfig里面

defaultConfig {
      buildConfigField "String", "AUTO_TYPE", "umeng"
}

2.然后配置渠道包

productFlavors {
        abc {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "abc"]
            buildConfigField "String", "AUTO_TYPE", "360"
        }
    wandoujia {
           manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
           buildConfigField "String", "AUTO_TYPE", "umeng"
      }
       baiduyun {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baiduyun"]
            buildConfigField "String", "AUTO_TYPE", "baiduyun"
        }
      
    }

next

Build->Clean Project
Build->Rebuild Project

3.看看是否生成了这个文件
BuildConfig.png
文件夹名是根据你productFlavors下面最后一个渠道名生成的

Auto_Type.png
生成了我们想要的文件,就可以运用了

if (ConstantField.AUTO_TYPE_BAIDU.equals(BuildConfig.AUTO_TYPE)){    
          BDAutoUpdateSDK.silenceUpdateAction(this);//百度静默更新    
          L.e(TAG,"baidu更新");
    }else if(ConstantField.AUTO_TYPE_360.equals(BuildConfig.AUTO_TYPE)){    
          UpdateManager.checkUpdate(this);    
          L.e(TAG,"360更新");
    }else{    
          UmengUpdateAgent.update(this);    
          L.e(TAG,"umeng更新");
}

添加是否打印log这个就无需再说了吧,方法类似,把string改成boolean即可
放两个所学习到的文章
转载注明来源即可

美团Android自动化之旅—适配渠道包
美团Android自动化之旅—生成渠道包

Android 中微信抢红包助手的实现

Android 中微信抢红包助手的实现

Android 中微信抢红包助手的实现

参考 (感谢作者):http://www.jianshu.com/p/cd1cd53909d7

http://blog.csdn.net/jiangwei0910410003/article/details/48895153

 

实现原理

  通过利用 AccessibilityService 辅助服务,监测屏幕内容,如监听状态栏的信息,屏幕跳转等,以此来实现自动拆红包的功能。关于 AccessibilityService 辅助服务,可以自行百度了解更多。

 

代码基础:

1. 首先声明一个 RedPacketService 继承自 AccessibilityService,该服务类有两个方法必须重写,如下:

/**
 * Created by cxk on 2017/2/3.
 * email:471497226@qq.com
 *
 * 抢红包服务类
 */

public class RedPacketService extends AccessibilityService {


    /**
     * 必须重写的方法:此方法用了接受系统发来的event。在你注册的event发生是被调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }

    /**
     * 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onInterrupt() {
        Toast.makeText(this, "我快被终结了啊-----", Toast.LENGTH_SHORT).show();
    }

    /**
     * 服务已连接
     */
    @Override
    protected void onServiceConnected() {
        Toast.makeText(this, "抢红包服务开启", Toast.LENGTH_SHORT).show();
        super.onServiceConnected();
    }

    /**
     * 服务已断开
     */
    @Override
    public boolean onUnbind(Intent intent) {
        Toast.makeText(this, "抢红包服务已被关闭", Toast.LENGTH_SHORT).show();
        return super.onUnbind(intent);
    }
}

2. 对我们的 RedPacketService 进行一些配置,这里配置方法可以选择代码动态配置(onServiceConnected 里配置),也可以直接在 res/xml 下新建.xml 文件,没有 xml 文件夹就新建。这里我们将文件命名为 redpacket_service_config.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/desc"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm" />

accessibilityEventTypes:   

响应哪一种类型的事件,typeAllMask 就是响应所有类型的事件了,另外还有单击、长按、滑动等。

accessibilityFeedbackType:  

用什么方式反馈给用户,有语音播出和振动。可以配置一些 TTS 引擎,让它实现发音。

packageNames:

指定响应哪个应用的事件。这里我们是写抢红包助手,就写微信的包名:com.tencent.mm,这样就可以监听微信产生的事件了。

notificationTimeout:

响应时间

description:

辅助服务的描述信息。

 

3.service 是四大组件之一,需要在 AndroidManifest 进行配置,注意这里稍微有些不同:

<!--抢红包服务-->
        <service
            android:name=".RedPacketService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/redpacket_service_config"></meta-data>
        </service>
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"  权限申请
android:resource="@xml/redpacket_service_config"  引用刚才的配置文件


核心代码:
我们的红包助手,核心思路分为三步走:
监听通知栏微信消息,如果弹出[微信红包]字样,模拟手指点击状态栏跳转到微信聊天界面→在微信聊天界面查找红包,如果找到则模拟手指点击打开,弹出打开红包界面→模拟手指点击红包“開”

1.监听通知栏消息,查看是否有[微信红包]字样,代码如下:
@Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //通知栏来信息,判断是否含有微信红包字样,是的话跳转
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                List<CharSequence> texts = event.getText();
                for (CharSequence text : texts) {
                    String content = text.toString();
                    if (!TextUtils.isEmpty(content)) {
                        //判断是否含有[微信红包]字样
                        if (content.contains("[微信红包]")) {
                            //如果有则打开微信红包页面
                            openWeChatPage(event);
                        }
                    }
                }
                break;
     }
 }

     /**
     * 开启红包所在的聊天页面
     */
    private void openWeChatPage(AccessibilityEvent event) {
        //A instanceof B 用来判断内存中实际对象A是不是B类型,常用于强制转换前的判断
        if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
            Notification notification = (Notification) event.getParcelableData();
            //打开对应的聊天界面
            PendingIntent pendingIntent = notification.contentIntent;
            try {
                pendingIntent.send();
            } catch (PendingIntent.CanceledException e) {
                e.printStackTrace();
            }
        }
    }
2.判断当前是否在微信聊天页面,是的话遍历当前页面各个控件,找到含有微信红包或者领取红包的textview控件,然后逐层找到他的可点击父布局(图中绿色部分),模拟点击跳转到含有“開”的红包界面,代码如下:

@Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //窗口发生改变时会调用该事件
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
                //判断是否是微信聊天界面
                if ("com.tencent.mm.ui.LauncherUI".equals(className)) {
                    //获取当前聊天页面的根布局
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始找红包
                    findRedPacket(rootNode);
                }
        }
    }
    /**
     * 遍历查找红包
     */
    private void findRedPacket(AccessibilityNodeInfo rootNode) {
        if (rootNode != null) {
            //从最后一行开始找起
            for (int i = rootNode.getChildCount() - 1; i >= 0; i--) {
                AccessibilityNodeInfo node = rootNode.getChild(i);
                //如果node为空则跳过该节点
                if (node == null) {
                    continue;
                }
                CharSequence text = node.getText();
                if (text != null && text.toString().equals("领取红包")) {
                    AccessibilityNodeInfo parent = node.getParent();
                    //while循环,遍历"领取红包"的各个父布局,直至找到可点击的为止
                    while (parent != null) {
                        if (parent.isClickable()) {
                            //模拟点击
                            parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                            //isOpenRP用于判断该红包是否点击过
                            isOpenRP = true;
                            break;
                        }
                        parent = parent.getParent();
                    }
                }
                //判断是否已经打开过那个最新的红包了,是的话就跳出for循环,不是的话继续遍历
                if (isOpenRP) {
                    break;
                } else {
                    findRedPacket(node);
                }

            }
        }
    }

3. 点击红包后,在模拟手指点击 “開” 以此开启红包,跳转到红包详情界面,方法与步骤二类似:

@Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //窗口发生改变时会调用该事件
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
          
                //判断是否是显示‘开’的那个红包界面
                if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI".equals(className)) {
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始抢红包
                    openRedPacket(rootNode);
                }
                break;
        }
    }

    /**
     * 开始打开红包
     */
    private void openRedPacket(AccessibilityNodeInfo rootNode) {
        for (int i = 0; i < rootNode.getChildCount(); i++) {
            AccessibilityNodeInfo node = rootNode.getChild(i);
            if ("android.widget.Button".equals(node.getClassName())) {
                node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
            openRedPacket(node);
        }
    }

结合以上三步,下面是完整代码,注释已经写的很清楚,直接看代码:

package com.cxk.redpacket;

import android.accessibilityservice.AccessibilityService;
import android.app.Instrumentation;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;

import java.util.List;

/**
 * 抢红包Service,继承AccessibilityService
 */
public class RedPacketService extends AccessibilityService {
    /**
     * 微信几个页面的包名+地址。用于判断在哪个页面
     * LAUCHER-微信聊天界面
     * LUCKEY_MONEY_RECEIVER-点击红包弹出的界面
     * LUCKEY_MONEY_DETAIL-红包领取后的详情界面
     */
    private String LAUCHER = "com.tencent.mm.ui.LauncherUI";
    private String LUCKEY_MONEY_DETAIL = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI";
    private String LUCKEY_MONEY_RECEIVER = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI";

    /**
     * 用于判断是否点击过红包了
     */
    private boolean isOpenRP;

    private boolean isOpenDetail=false;

    /**
     * 用于判断是否屏幕是亮着的
     */
    private boolean isScreenOn;

    /**
     * 获取PowerManager.WakeLock对象
     */
    private  PowerManager.WakeLock wakeLock;

    /**KeyguardManager.KeyguardLock对象
     *
     */
    private KeyguardManager.KeyguardLock keyguardLock;

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //通知栏来信息,判断是否含有微信红包字样,是的话跳转
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                Log.e("SSSSSS0","通知栏来信息TYPE_WINDOW_STATE_CHANGED");
                List<CharSequence> texts = event.getText();
                for (CharSequence text : texts) {
                    String content = text.toString();
                    if (!TextUtils.isEmpty(content)) {
                        //判断是否含有[微信红包]字样
                        if (content.contains("[微信红包]")) {
                            if(!isScreenOn()){
                                wakeUpScreen();
                            }
                            //如果有则打开微信红包页面
                            openWeChatPage(event);

                            isOpenRP = false;
                        }
                    }
                }
                break;
            //界面跳转的监听
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
                //判断是否是微信聊天界面
                if (LAUCHER.equals(className)) {
                    //获取当前聊天页面的根布局
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始找红包
                    findRedPacket(rootNode);
                }

                //判断是否是显示‘开’的那个红包界面
                if (LUCKEY_MONEY_RECEIVER.equals(className)) {
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始抢红包
                    openRedPacket(rootNode);
                }

                //判断是否是红包领取后的详情界面
                if (isOpenDetail&&LUCKEY_MONEY_DETAIL.equals(className)) {

                    isOpenDetail=false;
                    //返回桌面
                    back2Home();
                }
                break;
        }

        release();

    }

    /**
     * 开始打开红包
     */
    private void openRedPacket(AccessibilityNodeInfo rootNode) {
        for (int i = 0; i < rootNode.getChildCount(); i++) {
            AccessibilityNodeInfo node = rootNode.getChild(i);
            if ("android.widget.Button".equals(node.getClassName())) {
                node.performAction(AccessibilityNodeInfo.ACTION_CLICK);

                isOpenDetail=true;
            }
            openRedPacket(node);
        }
    }

    /**
     * 遍历查找红包
     */
    private void findRedPacket(AccessibilityNodeInfo rootNode) {
        if (rootNode != null) {
            //从最后一行开始找起
            for (int i = rootNode.getChildCount() - 1; i >= 0; i--) {
                AccessibilityNodeInfo node = rootNode.getChild(i);
                //如果node为空则跳过该节点
                if (node == null) {
                    continue;
                }
                CharSequence text = node.getText();
                if (text != null && text.toString().equals("领取红包")) {
                    AccessibilityNodeInfo parent = node.getParent();
                    //while循环,遍历"领取红包"的各个父布局,直至找到可点击的为止
                    while (parent != null) {
                        if (parent.isClickable()) {
                            //模拟点击
                            parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                            //isOpenRP用于判断该红包是否点击过
                            isOpenRP = true;
                            break;
                        }
                        parent = parent.getParent();
                    }
                }
                //判断是否已经打开过那个最新的红包了,是的话就跳出for循环,不是的话继续遍历
                if (isOpenRP) {
                    break;
                } else {
                    findRedPacket(node);
                }

            }
        }
    }

    /**
     * 开启红包所在的聊天页面
     */
    private void openWeChatPage(AccessibilityEvent event) {
        //A instanceof B 用来判断内存中实际对象A是不是B类型,常用于强制转换前的判断
        if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
            Notification notification = (Notification) event.getParcelableData();
            //打开对应的聊天界面
            PendingIntent pendingIntent = notification.contentIntent;
            try {
                pendingIntent.send();
            } catch (PendingIntent.CanceledException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 服务连接
     */
    @Override
    protected void onServiceConnected() {
        Toast.makeText(this, "抢红包服务开启", Toast.LENGTH_SHORT).show();
        super.onServiceConnected();
    }

    /**
     * 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onInterrupt() {
        Toast.makeText(this, "我快被终结了啊-----", Toast.LENGTH_SHORT).show();
    }

    /**
     * 服务断开
     */
    @Override
    public boolean onUnbind(Intent intent) {
        Toast.makeText(this, "抢红包服务已被关闭", Toast.LENGTH_SHORT).show();
        return super.onUnbind(intent);
    }

    /**
     * 返回桌面
     */
    private void back2Home() {
        Intent home = new Intent(Intent.ACTION_MAIN);
        home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        home.addCategory(Intent.CATEGORY_HOME);
        startActivity(home);
    }

    /**
     * 判断是否处于亮屏状态
     *
     * @return true-亮屏,false-暗屏
     */
    private boolean isScreenOn() {
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        isScreenOn = pm.isInteractive();
        return isScreenOn;
    }

    /**
     * 解锁屏幕
     */
    private void wakeUpScreen() {

        //获取电源管理器对象
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        //后面的参数|表示同时传入两个值,最后的是调试用的Tag
        wakeLock = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_DIM_WAKE_LOCK, "bright");

        //点亮屏幕
        wakeLock.acquire();

        //得到键盘锁管理器
        KeyguardManager km= (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
        keyguardLock=km.newKeyguardLock("unlock");

        //解锁
        keyguardLock.disableKeyguard();
    }

    /**
     * 释放keyguardLock和wakeLock
     */
    public void release(){
        if(wakeLock!=null){
            wakeLock.release();
            wakeLock=null;
        }
        if(keyguardLock!=null){
//            keyguardLock.reenableKeyguard();
            keyguardLock=null;
        }
    }

}

 

使用方法:

  设置 - 辅助功能 - 无障碍 - 点击 RedPacket 开启即可

 

已知问题:

1. 聊天列表或者聊天界面中无法直接自动抢红包

Demo 下载地址:https://github.com/CKTim/RedPacket

Android 视图绘制流程完全解析,带你一步步深入了解 View (二)

Android 视图绘制流程完全解析,带你一步步深入了解 View (二)

在上一篇文章中,我带着大家一起剖析了一下 LayoutInflater 的工作原理,可以算是对 View 进行深入了解的第一步吧。那么本篇文章中,我们将继续对 View 进行深入探究,看一看它的绘制流程到底是什么样的。如果你还没有看过我的上一篇文章,可以先去阅读 Android LayoutInflater 原理分析,带你一步步深入了解 View (一)  。

相信每个 Android 程序员都知道,我们每天的开发工作当中都在不停地跟 View 打交道,Android 中的任何一个布局、任何一个控件其实都是直接或间接继承自 View 的,如 TextView、Button、ImageView、ListView 等。这些控件虽然是 Android 系统本身就提供好的,我们只需要拿过来使用就可以了,但你知道它们是怎样被绘制到屏幕上的吗?多知道一些总是没有坏处的,那么我们赶快进入到本篇文章的正题内容吧。

要知道,任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即 onMeasure ()、onLayout () 和 onDraw (),下面我们逐个对这三个阶段展开进行探讨。

一. onMeasure ()

measure 是测量的意思,那么 onMeasure () 方法顾名思义就是用于测量视图的大小的。View 系统的绘制流程会从 ViewRoot 的 performTraversals () 方法中开始,在其内部调用 View 的 measure () 方法。measure () 方法接收两个参数,widthMeasureSpec 和 heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。

MeasureSpec 的值由 specSize 和 specMode 共同组成的,其中 specSize 记录的是大小,specMode 记录的是规格。specMode 一共有三种类型,如下所示:

1. EXACTLY

表示父视图希望子视图的大小应该是由 specSize 的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

2. AT_MOST

表示子视图最多只能是 specSize 中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过 specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

3. UNSPECIFIED

表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

那么你可能会有疑问了,widthMeasureSpec 和 heightMeasureSpec 这两个值又是从哪里得到的呢?通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。但是最外层的根视图,它的 widthMeasureSpec 和 heightMeasureSpec 又是从哪里得到的呢?这就需要去分析 ViewRoot 中的源码了,观察 performTraversals () 方法可以发现如下代码:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

可以看到,这里调用了 getRootMeasureSpec () 方法去获取 widthMeasureSpec 和 heightMeasureSpec 的值,注意方法中传入的参数,其中 lp.width 和 lp.height 在创建 ViewGroup 实例的时候就被赋值了,它们都等于 MATCH_PARENT。然后看下 getRootMeasureSpec () 方法中的代码,如下所示:

private int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

 

可以看到,这里使用了 MeasureSpec.makeMeasureSpec () 方法来组装一个 MeasureSpec,当 rootDimension 参数等于 MATCH_PARENT 的时候,MeasureSpec 的 specMode 就等于 EXACTLY,当 rootDimension 等于 WRAP_CONTENT 的时候,MeasureSpec 的 specMode 就等于 AT_MOST。并且 MATCH_PARENT 和 WRAP_CONTENT 时的 specSize 都是等于 windowSize 的,也就意味着根视图总是会充满全屏的。

介绍了这么多 MeasureSpec 相关的内容,接下来我们看下 View 的 measure () 方法里面的代码吧,如下所示:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {
        mPrivateFlags &= ~MEASURED_DIMENSION_SET;
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
        }
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }
        mPrivateFlags |= LAYOUT_REQUIRED;
    }
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}

注意观察,measure () 这个方法是 final 的,因此我们无法在子类中去重写这个方法,说明 Android 是不允许我们改变 View 的 measure 框架的。然后在第 9 行调用了 onMeasure () 方法,这里才是真正去测量并设置 View 大小的地方,默认会调用 getDefaultSize () 方法来获取视图的大小,如下所示:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

这里传入的 measureSpec 是一直从 measure () 方法中传递过来的。然后调用 MeasureSpec.getMode () 方法可以解析出 specMode,调用 MeasureSpec.getSize () 方法可以解析出 specSize。接下来进行判断,如果 specMode 等于 AT_MOST 或 EXACTLY 就返回 specSize,这也是系统默认的行为。之后会在 onMeasure () 方法中调用 setMeasuredDimension () 方法来设定测量出的大小,这样一次 measure 过程就结束了。

当然,一个界面的展示可能会涉及到很多次的 measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次 measure 过程。ViewGroup 中定义了一个 measureChildren () 方法来去测量子视图的大小,如下所示:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

这里首先会去遍历当前布局下的所有子视图,然后逐个调用 measureChild () 方法来测量相应子视图的大小,如下所示:

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到,在第 4 行和第 6 行分别调用了 getChildMeasureSpec () 方法来去计算子视图的 MeasureSpec,计算的依据就是布局文件中定义的 MATCH_PARENT、WRAP_CONTENT 等值,这个方法的内部细节就不再贴出。然后在第 8 行调用子视图的 measure () 方法,并把计算出的 MeasureSpec 传递进去,之后的流程就和前面所介绍的一样了。

当然,onMeasure () 方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:

public class MyView extends View {

	......
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		setMeasuredDimension(200, 200);
	}

}

这样的话就把 View 默认的测量流程覆盖掉了,不管在布局文件中定义 MyView 这个视图的大小是多少,最终在界面上显示的大小都将会是 200*200。

需要注意的是,在 setMeasuredDimension () 方法调用之后,我们才能使用 getMeasuredWidth () 和 getMeasuredHeight () 来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是 0。

由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在 XML 文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

到此为止,我们就把视图绘制流程的第一阶段分析完了。

二. onLayout ()

measure 过程结束后,视图的大小就已经测量好了,接下来就是 layout 的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot 的 performTraversals () 方法会在 measure 结束后继续执行,并调用 View 的 layout () 方法来执行此过程,如下所示:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

layout () 方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了 layout () 方法中。那么我们来看下 layout () 方法中的代码是什么样的吧,如下所示:

public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
        if (mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
}

在 layout () 方法中,首先会调用 setFrame () 方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给 mLeft、mTop、mRight 和 mBottom 这几个变量。接下来会在第 11 行调用 onLayout () 方法,正如 onMeasure () 方法中的默认行为一样,也许你已经迫不及待地想知道 onLayout () 方法中的默认行为是什么样的了。进入 onLayout () 方法,咦?怎么这是个空方法,一行代码都没有?!

没错,View 中的 onLayout () 方法就是一个空方法,因为 onLayout () 过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,我们来看下 ViewGroup 中的 onLayout () 方法是怎么写的吧,代码如下:

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

可以看到,ViewGroup 中的 onLayout () 方法竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法。没错,像 LinearLayout、RelativeLayout 等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。由于 LinearLayout 和 RelativeLayout 的布局规则都比较复杂,就不单独拿出来进行分析了,这里我们尝试自定义一个布局,借此来更深刻地理解 onLayout () 的过程。

 

自定义的这个布局目标很简单,只要能够包含一个子视图,并且让子视图正常显示出来就可以了。那么就给这个布局起名叫做 SimpleLayout 吧,代码如下所示:

public class SimpleLayout extends ViewGroup {

	public SimpleLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		if (getChildCount() > 0) {
			View childView = getChildAt(0);
			measureChild(childView, widthMeasureSpec, heightMeasureSpec);
		}
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		if (getChildCount() > 0) {
			View childView = getChildAt(0);
			childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
		}
	}

}

代码非常的简单,我们来看下具体的逻辑吧。你已经知道,onMeasure () 方法会在 onLayout () 方法之前调用,因此这里在 onMeasure () 方法中判断 SimpleLayout 中是否有包含一个子视图,如果有的话就调用 measureChild () 方法来测量出子视图的大小。

 

接着在 onLayout () 方法中同样判断 SimpleLayout 是否有包含一个子视图,然后调用这个子视图的 layout () 方法来确定它在 SimpleLayout 布局中的位置,这里传入的四个参数依次是 0、0、childView.getMeasuredWidth () 和 childView.getMeasuredHeight (),分别代表着子视图在 SimpleLayout 中左上右下四个点的坐标。其中,调用 childView.getMeasuredWidth () 和 childView.getMeasuredHeight () 方法得到的值就是在 onMeasure () 方法中测量出的宽和高。

这样就已经把 SimpleLayout 这个布局定义好了,下面就是在 XML 文件中使用它了,如下所示:

<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
	
    <ImageView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher"
        />
    
</com.example.viewtest.SimpleLayout>

可以看到,我们能够像使用普通的布局文件一样使用 SimpleLayout,只是注意它只能包含一个子视图,多余的子视图会被舍弃掉。这里 SimpleLayout 中包含了一个 ImageView,并且 ImageView 的宽高都是 wrap_content。现在运行一下程序,结果如下图所示:

 

                               

OK!ImageView 成功已经显示出来了,并且显示的位置也正是我们所期望的。如果你想改变 ImageView 显示的位置,只需要改变 childView.layout () 方法的四个参数就行了。

在 onLayout () 过程结束后,我们就可以调用 getWidth () 方法和 getHeight () 方法来获取视图的宽高了。说到这里,我相信很多朋友长久以来都会有一个疑问,getWidth () 方法和 getMeasureWidth () 方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。

首先 getMeasureWidth () 方法在 measure () 过程结束后就可以获取到了,而 getWidth () 方法要在 layout () 过程结束后才能获取到。另外,getMeasureWidth () 方法中的值是通过 setMeasuredDimension () 方法来进行设置的,而 getWidth () 方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

观察 SimpleLayout 中 onLayout () 方法的代码,这里给子视图的 layout () 方法传入的四个参数分别是 0、0、childView.getMeasuredWidth () 和 childView.getMeasuredHeight (),因此 getWidth () 方法得到的值就是 childView.getMeasuredWidth () - 0 = childView.getMeasuredWidth () ,所以此时 getWidth () 方法和 getMeasuredWidth () 得到的值就是相同的,但如果你将 onLayout () 方法中的代码进行如下修改:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
	if (getChildCount() > 0) {
		View childView = getChildAt(0);
		childView.layout(0, 0, 200, 200);
	}
}

这样 getWidth () 方法得到的值就是 200 - 0 = 200,不会再和 getMeasuredWidth () 的值相同了。当然这种做法充分不尊重 measure () 过程计算出的结果,通常情况下是不推荐这么写的。getHeight () 与 getMeasureHeight () 方法之间的关系同上,就不再重复分析了。

 

到此为止,我们把视图绘制流程的第二阶段也分析完了。

 

三. onDraw ()

measure 和 layout 的过程都结束后,接下来就进入到 draw 的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。ViewRoot 中的代码会继续执行并创建出一个 Canvas 对象,然后调用 View 的 draw () 方法来执行具体的绘制工作。draw () 方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:

public void draw(Canvas canvas) {
	if (ViewDebug.TRACE_HIERARCHY) {
	    ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
	}
	final int privateFlags = mPrivateFlags;
	final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
	        (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
	mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
	// Step 1, draw the background, if needed
	int saveCount;
	if (!dirtyOpaque) {
	    final Drawable background = mBGDrawable;
	    if (background != null) {
	        final int scrollX = mScrollX;
	        final int scrollY = mScrollY;
	        if (mBackgroundSizeChanged) {
	            background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
	            mBackgroundSizeChanged = false;
	        }
	        if ((scrollX | scrollY) == 0) {
	            background.draw(canvas);
	        } else {
	            canvas.translate(scrollX, scrollY);
	            background.draw(canvas);
	            canvas.translate(-scrollX, -scrollY);
	        }
	    }
	}
	final int viewFlags = mViewFlags;
	boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
	boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
	if (!verticalEdges && !horizontalEdges) {
	    // Step 3, draw the content
	    if (!dirtyOpaque) onDraw(canvas);
	    // Step 4, draw the children
	    dispatchDraw(canvas);
	    // Step 6, draw decorations (scrollbars)
	    onDrawScrollBars(canvas);
	    // we''re done...
	    return;
	}
}

可以看到,第一步是从第 9 行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个 mBGDrawable 对象,然后根据 layout 过程确定的视图位置来设置背景的绘制区域,之后再调用 Drawable 的 draw () 方法来完成背景的绘制工作。那么这个 mBGDrawable 对象是从哪里来的呢?其实就是在 XML 中通过 android:background 属性设置的图片或颜色。当然你也可以在代码中通过 setBackgroundColor ()、setBackgroundResource () 等方法进行赋值。

 

接下来的第三步是在第 34 行执行的,这一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下 onDraw () 方法,那么 onDraw () 方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。

第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现 View 中的 dispatchDraw () 方法又是一个空方法,而 ViewGroup 的 dispatchDraw () 方法中就会有具体的绘制代码。

以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是 ListView 或者 ScrollView,为什么要绘制滚动条呢?其实不管是 Button 也好,TextView 也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。

通过以上流程分析,相信大家已经知道,View 是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察 TextView、ImageView 等类的源码,你会发现它们都有重写 onDraw () 这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助 Canvas 这个类,它会作为参数传入到 onDraw () 方法中,供给每个视图使用。Canvas 这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,那么我们就来尝试一下吧。

这里简单起见,我只是创建一个非常简单的视图,并且用 Canvas 随便绘制了一点东西,代码如下所示:

public class MyView extends View {

	private Paint mPaint;

	public MyView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		mPaint.setColor(Color.YELLOW);
		canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
		mPaint.setColor(Color.BLUE);
		mPaint.setTextSize(20);
		String text = "Hello View";
		canvas.drawText(text, 0, getHeight() / 2, mPaint);
	}
}

可以看到,我们创建了一个自定义的 MyView 继承自 View,并在 MyView 的构造函数中创建了一个 Paint 对象。Paint 就像是一个画笔一样,配合着 Canvas 就可以进行绘制了。这里我们的绘制逻辑比较简单,在 onDraw () 方法中先是把画笔设置成黄色,然后调用 Canvas 的 drawRect () 方法绘制一个矩形。然后在把画笔设置成蓝色,并调整了一下文字的大小,然后调用 drawText () 方法绘制了一段文字。

 

就这么简单,一个自定义的视图就已经写好了,现在可以在 XML 中加入这个视图,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.viewtest.MyView 
        android:layout_width="200dp"
        android:layout_height="100dp"
        />

</LinearLayout>

将 MyView 的宽度设置成 200dp,高度设置成 100dp,然后运行一下程序,结果如下图所示:

 

                      

图中显示的内容也正是 MyView 这个视图的内容部分了。由于我们没给 MyView 设置背景,因此这里看不出来 View 自动绘制的背景效果。

当然了 Canvas 的用法还有很多很多,这里我不可能把 Canvas 的所有用法都列举出来,剩下的就要靠大家自行去研究和学习了。

到此为止,我们把视图绘制流程的第三阶段也分析完了。整个视图的绘制过程就全部结束了,你现在是不是对 View 的理解更加深刻了呢?感兴趣的朋友可以继续阅读 Android 视图状态及重绘流程分析,带你一步步深入了解 View (三) 。

我们今天的关于教你一步步实现Android微信自动抢红包教你一步步实现android微信自动抢红包功能的分享就到这里,谢谢您的阅读,如果想了解更多关于Androd强化抢红包实现不了自动抢红包、Android Studio一步步教你集成发布适配、Android 中微信抢红包助手的实现、Android 视图绘制流程完全解析,带你一步步深入了解 View (二)的相关信息,可以在本站进行搜索。

本文标签: