android 如何在三方Sdk的Activity中增加自己的布局并获取特定的滑动事件

项目中会添加各种三方的sdk,大部分都是只有功能,没有 Activity 页面的,但是也有例外,比如说如果项目中接入了某些广告类型的Sdk,打开广告时,此时的Activity 就是Sdk中自带的,我们一般都是需要在自己项目中的配置清单 AndroidManifest 配置一下activity的路径名称。 前几天接到了个需求,要在某三方广告页面增加一个自己的布局,并且手指在广告页面滑动时,要触发自己布局中的某个空间,让它旋转一下。接到需求时才知道,Ios说做不到,所以只有Android实现此功能,只能感叹我司的产品同学,脑路一个比一个大,心中抱怨几句,但还是要开动脑子去实现。

问题一分为二:一、如何把布局view添加到三方SDk的Activity上;二、怎么监听到手指头在页面广告页面滑动了。 先看问题一 

如果是自己的Activity页面,布局里增加一个view再简单不过了,直接写在 layout 布局中或者动态的 addView() 都可以,但是这个是别人的页面,并且里面的布局是用初代插件化的方式动态加载的,我想直接替换它的layout布局都不行,既然替换不行,那么我可不可以动态的增加布局呢?我只要能定位到广告的 Activity 即可。说来也巧了,之前我优化过如何判断页面是出于前端还是切入后台的功能,原代码是通过获取 ActivityManager 来判断,如果经常性的调用,对性能不太好,代码如下

    public static boolean isAppOnForeground(final Context context) {
        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        String packageName = context.getPackageName();
        List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
        if (appProcesses == null){
            return false;
        }
        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
            if (appProcess.processName.equals(packageName)
                    && appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                return true;
            }
        }
        return false;
    }

我们可以通过 ActivityManager 类来获取最顶层的 Activity,判断它是不是广告页面,但这种方法比较笨重,因为我们每次打开一个新的页面,都要判断一下,这个比较被动,舍弃。我之前优化判断是否切入后台的思路,是通过 Application 来判断,Application 是全局的,它能管理项目中所有的 Activity,所以我给它注册了一个activity的声明周期回调,代码如下

class BaseApplication extends Application{

    public final static String TAG = "BaseApplication";
    private int mActivityCount;

    @Override
    public void onCreate() {
        super.onCreate();

    }

    private void registerActivity() {
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

            }

            @Override
            public void onActivityStarted(Activity activity) {
                mActivityCount++;
                //如果mFinalCount ==1,说明是从后台到前台
                Log.e(TAG, mActivityCount + "    onActivityStarted  " + activity);
                if (mActivityCount == 1) {
                    //说明从后台回到了前台
                    Log.e(TAG, " 从后台到前台    " );
                }
            }

            @Override
            public void onActivityResumed(Activity activity) {

            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {
                mActivityCount--;
                //如果mFinalCount ==0,说明是前台到后台
                Log.e(TAG, mActivityCount + "    onActivityStopped  " + activity);
                if (mActivityCount == 0) {
                    //说明从前台回到了后台
                    Log.e(TAG, " 前台回到了后台  " );
                }
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {

            }
        });
    }

}

Activity的声明周期,比如 onResume()的流程,会从 ActivityThread 中 performResumeActivity() ----- Activity 的 performResume() ---- Instrumentation 的 callActivityOnResume() ----Activity 的 onResume() ---- Application 的 dispatchActivityResumed() ,在这个方法中,会遍历注册的声明周期回调,即ActivityLifecycleCallbacks 的 onActivityResumed() 方法,

既然是这样,那么我就在Application的回调中,判断 Activity 的类型,如果是广告页面,我们就想办法添加布局,对于一个Activity,如果我们知道layout根布局,就可以了,但可惜,我们不知道,并且广告是三方的,它随时可以改变根布局。看过 PhoneWindow 的同学,都知道,不管什么Activity,它默认都有个布局,id 为 com.android.internal.R.id.content,它是layout的父容器,知道这一点,就解决了一大半,还有一小半是添加 view 的时机问题,一般情况,我们如果单纯的添加一个view,可以直接添加;但如果我们想替换某个控件,尤其这个控件还是广告页面通过动态加载添加的,那么我们就得考虑一下时机了。不管是否通过 setContentView(R.layout.activity) 这种方法添加布局,它都会通过 PhoneWindow 来管理布局, ActivityThread 的 handleResumeActivity() 方法中,

            ActivityClientRecord r = performResumeActivity(token, clearHide);
            ...
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }
            }
我们重点关注 View decor = r.window.getDecorView() 这行代码,Activity 中如果没有调用 setContentView() 方法,那么在这里 getDecorView() 方法中就会创建 Activity 的根布局 DecorView,同时把 id 为 com.android.internal.R.id.content 的 FrameLayout所在的布局也添加进去,通过上面代码,它是在 performResumeActivity() 之后执行的, 也就是在 onResume() 之后,所以这里有个小细节,如果当前Activity 没有调用过 setContentView() 方法, 我们在 Application 中 onActivityCreated() 中直接 findViewById(Window.ID_ANDROID_CONTENT) 可能会有问题的,除非 Activity 中有调用过 getWindow().getDecorView(),我们为了保险起见,同时也为了规范点,我们等 handleResumeActivity() 逻辑执行完了再去执行自己的逻辑,什么时候执行完呢?我们可以使用 getWindow().getDecorView().post() 方法,这样就巧妙的把上面的两个要求都实现了 

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        addViewToContent(activity);
    }        

    public void addViewToContent(final Activity activity){
        if (activity instanceof AdActivity) {
            activity.getWindow().getDecorView().post(new Runnable() {
                @Override
                public void run() {
                    FrameLayout frameLayout = (FrameLayout) activity.findViewById(Window.ID_ANDROID_CONTENT);
                    View iv = new ImageView(activity);
                    iv.setImageResource(R.drawable.ad_close_icon);
                    FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(600,600);
                    iv.setLayoutParams(layoutParams);
                    frameLayout.addView(iv);
                }
            });
        }
    }
    
这样,我们就在Activity中成功的添加了自己的view。还有没其他方法,我想起了早期做的一个悬浮窗按钮,通过 WindowManager 来 addView(),所以就把以前的代码找了出了,写了方法二

    private static void addViewToContent2(Activity activity){
        final WindowManager mWindowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
        final Window w = activity.getWindow();
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
        mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;// 系统提示window
        mLayoutParams.format = PixelFormat.TRANSLUCENT;// 支持透明
        //mParams.format = PixelFormat.RGBA_8888;
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |       //该flags描述的是窗口的模式,是否可以触摸,可以聚焦等
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        mLayoutParams.width = 490;//窗口的宽和高
        mLayoutParams.height = 160;
        mLayoutParams.x = 0;//窗口位置的偏移量
        mLayoutParams.y = 0;
        final ImageView iv = new ImageView(activity);
        iv.setImageResource(R.drawable.ad_close_icon);
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(1000,1920);
        iv.setLayoutParams(layoutParams);
        mWindowManager.addView(iv, mLayoutParams);//添加窗口
    }

把 addViewToContent() 方法 替换为 addViewToContent2() 即可。 这两种方法,尤其是第二种,一定要在 Activity 销毁时,在 Application 的 onActivityDestroyed() 方法中调用,移除 iv布局,原因是 方法一是添加到 Activity 的布局里面, Activity 销毁时 iv 也就不显示了;方法二是添加到窗口上了,如果 Activity 销毁时 iv 没有被移除,则它会一直显示在窗口上,容易内存泄漏和UI显示错误。


问题二  

如何监听到广告里面一次完整的手指触摸事件,即 按下-滑动-抬起 ,通过使用工具分析得知,该广告页面是用 WebView 加载的,webView 加载url,超出屏幕部分可以上下滑动。WebView 消费了触摸事件,通过 findViewById(Window.ID_ANDROID_CONTENT)可以找到 id 为 content 的 Fragment 容器,我们可以遍历它的子控件,层层找下去,找到 webView,对它设置 setOnTouchListener() 来监听触摸事件,这样就可以知道了。看起来万事大吉,但有几个隐患:1、如果广告页面的 webView 本身已经有设置 setOnTouchListener() 回调,我们在外面再次设置便会把原先覆盖,除非可以获取原先的 OnTouchListener ,我们在自己的setOnTouchListener() 回调中把它给放进去,形成一个代理模式,但可惜, View 中没有 getOnTouchListener() 这个 API,放弃; 2、万一里面的 WebView 不是一个,而是两个甚至三个怎么办?我司项目中,由于有些特殊的需求,需要一个页面添加2个WebView,这种场景罕见,但并非没有可能,如果有多个,我们就需要每个都设置回调,这是后就又回到了隐患1。所以这种方法可以,但不敢保证兼容所有。
换一种思路,对于 WebView 看过它的源码,它的 onTouchEvent() 方法会消费掉触摸事件,发现这一点,脑子里便有了模糊的思路。看过前面几篇关于事件分发机制消息的文章,我们知道,事件分发是从 Activity 传递到 window窗口的 DecorView 根布局,进而传递到 layout布局的 根布局,然后才是对应的子控件,如果没有消费,事件会传给上一层,看看是否消费,如果没有,继续上传上一层,最终传递给 Activity 。这里有个要点,就是事件的分发传递, Activity - ViewGroup - View,这三者之间传递,入口都是 dispatchTouchEvent() 方法,我们是否可以在它上面想些办法呢? Activity 的dispatchTouchEvent()方法我们没法重写,那么 DecorView 呢?我们似乎也没办法。对于WebView 呢,也不行。我们现在可以找到 id 为 content 的 Fragment 容器,如果我们在它和它的子控件之间添加一层自定义的 Fragment,或者用自定义的 Fragment 来替换 content 容器,这样,我们就可以重写dispatchTouchEvent() 方法,在它里面做操作了,为了尽量减轻布局嵌套,我们采用替换的方法,既然是替换,我们需要找到 content 的所有子控件,把它们移除掉,然后把这些子控件都添加到自己创建出来的容器,然后再替换,代码如下

    private void replaceView(Activity activity){
        FrameLayout frameLayout = (FrameLayout) activity.findViewById(Window.ID_ANDROID_CONTENT);
        ViewGroup parent = (ViewGroup) frameLayout.getParent();
        FrameLayout fl = new FrameLayout(activity);
        for(int i = 0, count = frameLayout.getChildCount(); i < count; i++){
            View view = frameLayout.getChildAt(i);
            frameLayout.removeView(view);
            fl.addView(view);
        }
        parent.removeView(frameLayout);
        parent.addView(fl);
        fl.setId(Window.ID_ANDROID_CONTENT);
    // 添加自己的控件
        ImageView iv = new ImageView(activity);
        iv.setImageResource(R.drawable.close_icon);
    fl.addView(iv);
    }

这样就完成了替换Fragment的功能,此时重点就落在了自定义控件上,它的 dispatchTouchEvent() 会接收到webview返回的事件值,webView的 onTouchEvent() 中的事件,Fragment中都会接收到。一个完整的事件,包括ACTION_DOWN、ACTION_UP、ACTION_MOVE,值分别是 0、1、2, Fragment 中 dispatchTouchEvent() 都会有触发,我们可以用 HashSet 来记录值,只要集合的元素个数为3,则满足一次触摸滑动事件,但集合毕竟消耗内存,尤其是 move 时,不停的add同一个元素,更是消耗;优化方案,既然是要保持唯一性,那我们用位移位运算即可,down up move 分别对应唯一一个二进制值,

class FlContentView extends FrameLayout{

    public FlContentView(Context context) {
        super(context);
    }

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

    public FlContentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        ViewHelper.transformFlag(event.getAction());
        return super.dispatchTouchEvent(event);
    }
}

class ViewAddHelper {

    private static final int FLAG_ACTION_DOWN = 1 << 0;
    private static final int FLAG_ACTION_UP = 1 << 1;
    private static final int FLAG_ACTION_MOVE = 1 << 2;
    private static final int FLAG_NUM_ADD = FLAG_ACTION_DOWN | FLAG_ACTION_UP | FLAG_ACTION_MOVE;

    private static int mFlag = 0;
    private static int mScrollNum;

    public static void transformFlag(int flag) {
        if (MotionEvent.ACTION_DOWN == flag) {
            addFlag(FLAG_ACTION_DOWN);
        } else if (MotionEvent.ACTION_UP == flag) {
            addFlag(FLAG_ACTION_UP, true);
        } else if (MotionEvent.ACTION_MOVE == flag) {
            addFlag(FLAG_ACTION_MOVE);
        }
    }

    private static void addFlag(int flag){
        addFlag(flag, false);
    }

    private static void addFlag(int flag, boolean allow){
        mFlag |= flag;
        if(mFlag == FLAG_NUM_ADD && allow){
        checkSide();
            restFlag();
        }
    }

    public static void restFlag(){
        mFlag = 0;
    }

    private static void checkSide(){
        Log.e("ViewAddHelper", " 滑动次数第  "  + (++mScrollNum)  + "  次");
    }

}

满足滑动条件后,马上清除标识,checkSide() 中我们可以做自己相应的逻辑操作,比如说每次滑动后就需要我们添加的 imageView 动画旋转一下,既可以在 checkSide() 中调用这个控件的动画方法。本来,到此就结束了,往自己项目中的三方页面添加自己的控件,并且三方页面布局滑动时,自己控件有动画旋转,上面例子中也满足了,但项目中又有点不同,因为添加自己的控件不是单纯的一个 ImageView,而是一个可以拖动滑动的 view,如果这个 view 大小铺满全屏,根据手指触摸滑动,https://blog.csdn.net/Deaht_Huimie/article/details/89161963  这篇文章中有介绍几种位移的方法,都是铺满全屏的。那么 WebView 则接受不到触摸事件,所以 view 不能铺满全屏,只能展示自己的部分,然后依附于父容器,手指头按住 view 时,再进行位移,布局结构类似

    <FrameLayout
        android:id="@+id/fl_move_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/iv_move"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#222222"
            />
    </FrameLayout>

https://blog.csdn.net/Deaht_Huimie/article/details/52817268  这里面也有几种位移的方法,适合这个布局的例子,手指按到 iv_move 上,会拖动它位移,如果 iv_move 没有消费事件。问题就出在这,由于它消费了事件,它也是 FlContentView 控件的子控件,那么 FlContentView 的 dispatchTouchEvent() 方法也会接受到完整的事件触摸机制,这是后就会产生一个状况,不论是滑动 webView 还是可以拖动的 iv, FlContentView 的 dispatchTouchEvent() 中都会打印滑动log,触发旋转动画,怎么办?如果能辨别是触发的控件是哪个就好了,但可惜,这两个页面,一个是三方的,别想了;另外一个是自己的,但它是另外一个小组的,我这边只负责把它给添加到广告页面并触发旋转动画。我一开始想重写一下 iv_move 的 dispatchTouchEvent() 方法,在里面调用 ViewHelper.restFlag() 方法,但是这个控件好多地方都有用到,都是统一控制,也不方便写子类,意思就是这也类似是个黑盒,我无权去改动它里面的内容。怎么办?既然不能改动,那么只能在外面判断了,只要 iv_move 消费了事件,它的父容器都能监听到,那么我就把它添加到 FlContentView 容器之前,再给它增加一层父布局,在父布局中接收消息进行判断,如果子view消费了事件,那么它的 dispatchTouchEvent(MotionEvent event) 中的 super.dispatchTouchEvent(event) 返回值是 true,于是又一个自定义控件诞生了

class FlImageParentView extends FrameLayout{

    public FlImageParentView(Context context) {
        super(context);
    }

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

    public FlImageParentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean handled = super.dispatchTouchEvent(event);
        if(handled){
            ViewHelper.restFlag();
        }
        return handled;
    }
}

最终的添加布局的代码就变为了下面的代码  

    private void addReplaceView(Activity activity){
        FrameLayout frameLayout = (FrameLayout) activity.findViewById(Window.ID_ANDROID_CONTENT);
        ViewGroup parent = (ViewGroup) frameLayout.getParent();
        FrameLayout fl = new FrameLayout(activity);
        for(int i = 0, count = frameLayout.getChildCount(); i < count; i++){
            View view = frameLayout.getChildAt(i);
            frameLayout.removeView(view);
            fl.addView(view);
        }
        parent.removeView(frameLayout);
        parent.addView(fl);
        fl.setId(Window.ID_ANDROID_CONTENT);
        // 添加自己的控件
        ImageView iv = new ImageView(activity);
        iv.setImageResource(R.drawable.close_icon);
        FlImageParentView fILayout = new FlImageParentView(activity);
        fILayout.addView(iv);
        fl.addView(fILayout);
    }

这样,在 Application 中注册的生命周期回调方法,开启了iv的添加流程

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        addViewToContent(activity);
    }

    public void addViewToContent(final Activity activity){
        if (activity instanceof AdActivity) {
            activity.getWindow().getDecorView().post(new Runnable() {
                @Override
                public void run() {
                    addReplaceView(activity);
                }
            });
        }
    }

谨记一点,在 onActivityDestroyed(Activity activity) 方法中,记得要把添加的控件 view 从 content 中移除,防止由于内部逻辑可能造成内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值