ScopeView库开发的玉使用详解

@TOC
ScopeView库开发的与使用详解
前言
项目开发中需要实现图片在指定的布局范围进行任意的移动。
现状分析与研究
现有的组件库无法满足项目中的需要,分别研究了PhotoView、 Zoomy和ZoomHelper,三个库分别涵盖所需要的不同场景,因为项目主要为Java语言,这里计划将PhotoView和Zoomy进行结合后进行二次开发。
ScopeView的诞生
既然是库的开发,那么必要有一个响亮的名字,这里命名为ScopeView。

创建新的代码工程
下载PhotoView项目代码
最组件的开发:
如何再ConstraintLayout中克隆它的子布局中的一个子View:
1,保持和原子View大小一致并且高度重叠;
2,克隆出来的新子View约束关系需要ConstraintLayout中而不是在它的子布局中;
在 ConstraintLayout 中克隆一个位于“孙子辈”(子布局内部)的 View,并让克隆体脱离原父容器、直接作为 ConstraintLayout 的直属子 View,同时保持完全重叠(坐标大小一致),可以通过动态代码计算绝对坐标 + 动态创建 View + 动态设置约束来实现。
动态克隆一个新的 View 实例Java 中无法直接“深度克隆”一个 View 对象,最稳妥的做法是:根据原 View 的类型,通过构造函数重新 new 一个新实例,并将关键属性(如背景、图片、缩放类型等)复制过去。

计算原 View 在屏幕上的绝对位置由于原 View 在子布局内部,直接获取它的 getLeft() 或 getX() 是相对它自己的父容器的,不能直接拿来给 ConstraintLayout 做约束。我们必须使用 getLocationOnScreen 计算其在屏幕上的绝对坐标,再转换到 ConstraintLayout 坐标系中。

动态配置 ConstraintLayout.LayoutParams 强行重叠获取到绝对长宽和坐标后,将新 View 的宽高设为固定值(与原 View 一致)。由于新 View 的约束必须直接绑定到 ConstraintLayout(即 parent),我们通过设置 topMargin 和 startMargin 将其精准平移到重叠位置。
如何再Activity的页面中快速克隆它的布局中的一个子View,并且能够完全覆盖遮挡主原来的子view呢?
在 Activity 页面中快速克隆一个子 View 并实现完美原位覆盖遮挡,最快且最不容易出错的方案是:直接使用 FrameLayout 作为承载克隆体的“临时画布”,利用窗口根布局(DecorView)的绝对坐标系进行图层覆盖。

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

/**

  • 快速克隆任意一个子 View,并在应用最顶层完美原位覆盖遮挡它

  • @param activity 当前的 Activity

  • @param targetView 想要被覆盖遮挡的目标子 View

  • @return 返回克隆出来的全新 View 实例(方便后续做动画或隐藏)
    */
    private View cloneAndOverlayView(Activity activity, View targetView) {
    if (activity == null || targetView == null) return null;

    // 1. 核心魔术:通过“截图”原 View,完美克隆其所有视觉细节(包括文字、图片、阴影、三方自定义样式等)
    Bitmap bitmap = Bitmap.createBitmap(targetView.getWidth(), targetView.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    targetView.draw(canvas);

    // 2. 创建一个全新的 ImageView 来承载这个克隆的“全息影像”
    ImageView clonedView = new ImageView(activity);
    clonedView.setImageDrawable(new BitmapDrawable(activity.getResources(), bitmap));

    // 3. 计算原 View 在整张屏幕/窗口中的绝对坐标
    int[] location = new int[2];
    targetView.getLocationInWindow(location); // 获取相对窗口的绝对坐标
    int absoluteX = location[0];
    int absoluteY = location[1];

    // 4. 获取 Activity 的顶级根容器(DecorView 内部的 content 容器,位于所有布局最顶层)
    ViewGroup windowRoot = activity.findViewById(android.R.id.content);

    // 5. 配置绝对定位参数,让克隆体完美重叠在原位置
    FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(targetView.getWidth(), targetView.getHeight());
    lp.leftMargin = absoluteX;
    lp.topMargin = absoluteY;

    // 6. 塞入顶层容器,完成完美覆盖
    windowRoot.addView(clonedView, lp);

    return clonedView; // 返回克隆体,供你后续使用(例如:clonedView.setVisibility(View.GONE))
    }
    如何再Activity的页面中快速克隆它的布局中的一个子View放置指定的ConstraintLayout布局中,并且能够完全覆盖遮挡主原来的子view呢?

import android.view.View;
import android.widget.ImageView; // 假设原 View 是 ImageView,可根据实际类型修改
import androidx.constraintlayout.widget.ConstraintLayout;

/**

  • 在 ConstraintLayout 中克隆孙子辈的 View

  • @param parentLayout 顶层的 ConstraintLayout

  • @param targetChildView 被克隆的目标子View(在子布局内部)
    */
    private void cloneAndOverlayView(ConstraintLayout parentLayout, View targetChildView) {
    if (parentLayout == null || targetChildView == null) return;

    // 1. 获取原 View 在屏幕上的绝对坐标
    int[] targetLocation = new int[2];
    targetChildView.getLocationOnScreen(targetLocation);
    int targetX = targetLocation[0];
    int targetY = targetLocation[1];

    // 2. 获取顶层 ConstraintLayout 在屏幕上的绝对坐标
    int[] parentLocation = new int[2];
    parentLayout.getLocationOnScreen(parentLocation);
    int parentX = parentLocation[0];
    int parentY = parentLocation[1];

    // 3. 计算出原 View 相对于 ConstraintLayout 的相对坐标
    int relativeX = targetX - parentX;
    int relativeY = targetY - parentY;

    // 4. 获取原 View 的实际宽高
    int targetWidth = targetChildView.getWidth();
    int targetHeight = targetChildView.getHeight();

    // 5. 动态创建克隆的 View 实例(此处以 ImageView 为例,可换成普通 View 或自定义 View)
    ImageView clonedView = new ImageView(parentLayout.getContext());

    // [关键点] 复制原 View 的关键视觉属性(根据你的 View 类型自行补充)
    clonedView.setId(View.generateViewId()); // 生成一个唯一的组件 ID
    if (targetChildView instanceof ImageView) {
    clonedView.setImageDrawable(((ImageView) targetChildView).getDrawable());
    clonedView.setScaleType(((ImageView) targetChildView).getScaleType());
    } else {
    clonedView.setBackground(targetChildView.getBackground());
    }

    // 6. 配置在 ConstraintLayout 中的约束关系
    ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(targetWidth, targetHeight);

    // 建立新 View 相对直属父布局(parent)的左上角约束
    lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
    lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;

    // 通过物理 Margin 将其精准定位到重叠坐标
    lp.leftMargin = relativeX;
    lp.topMargin = relativeY;

    // 兼容国际化方向
    lp.setMarginStart(relativeX);

    // 7. 将新 View 添加到 ConstraintLayout 中
    parentLayout.addView(clonedView, lp);
    }

帮我起个类的名字:含义是指父布局中的子View,但是父布局是外部传入,子View是根据外子View克隆的。
新增构造函数:

    public Builder(ViewGroup viewGroup) {
        this.mTargetContainer = new ViewGroupContainer(viewGroup);
    }

目前实现了View在指定范围内拖动并且放大
那么如何实现,当双指送开的时候,不反弹回去呢?
关键代码如下所示:

private Runnable mEndingZoomAction = new Runnable() {
    @Override
    public void run() {
        removeFromDecorView(mShadow);
        removeFromDecorView(mZoomableView);
        mTarget.setVisibility(View.VISIBLE);
        mZoomableView = null;
        mCurrentMovementMidPoint = new PointF();
        mInitialPinchMidPoint = new PointF();
        mAnimatingZoomEnding = false;
        mState = STATE_IDLE;

        if (mZoomListener != null) mZoomListener.onViewEndedZooming(mTarget);

        if (mConfig.isImmersiveModeEnabled()) showSystemUI();
    }
};

这行代码是 Zoomy 库对外提供的一个 getter 方法,用来查询当前是否开启了“松手缩回原位时的动画”。

public boolean isZoomAnimationEnabled() { return zoomAnimationEnabled; }
用来查询当前是否开启了“沉浸式放大模式”。在原版 Zoomy 的架构设计中,这个属性非常关键,它直接决定了图片克隆放大时,是否要突破屏幕最顶端的「状态栏(Status Bar)」和底部的「导航栏(Navigation Bar)」。

public boolean isImmersiveModeEnabled() {
return immersiveModeEnabled;
}
ZoomyConfig类中增加一个变量,并设置get和set方法,用于标识是否将手松开后将图片还原回去

package com.ablanco.zoomy;

public class ZoomyConfig {

// 🌟 二次开发新增变量:标识手松开后是否将图片自动还原回去。
// 默认为 true(保持原 Zoomy 行为);你在项目中实例化时可以将其设为 false
private boolean autoResetOnRelease = true; 

// 其他原有变量...
private boolean zoomAnimationEnabled = true;
private boolean immersiveModeEnabled = false;

// 🌟 新增变量的 Getter 方法
public boolean isAutoResetOnRelease() {
    return autoResetOnRelease;
}

// 🌟 新增变量的 Setter 方法
public void setAutoResetOnRelease(boolean autoResetOnRelease) {
    this.autoResetOnRelease = autoResetOnRelease;
}

// 原有的其他 get/set 方法...
public boolean isZoomAnimationEnabled() {
    return zoomAnimationEnabled;
}

public void setZoomAnimationEnabled(boolean zoomAnimationEnabled) {
    this.zoomAnimationEnabled = zoomAnimationEnabled;
}

}
如果当前的mTargetContainer是ViewGroupContainer的时候,我添加一个自定义的ImageView,否则继续沿用默认的逻辑

    if (mTargetContainer instanceof ViewGroupContainer) {
        mZoomableView = new PhotoView(mTarget.getContext());
    } else {
        mZoomableView = new ImageView(mTarget.getContext());
    }
    mZoomableView.setLayoutParams(new ViewGroup.LayoutParams(mTarget.getWidth(), mTarget.getHeight()));
    mZoomableView.setImageBitmap(ViewUtils.getBitmapFromView(view));

当view在拖动的过程中的时候,屏幕会有变暗的效果,这个如何去除?
ZoomyConfig类中增加一个变量,并设置get和set方法,用于标识是否拖动的时候有变暗效果,即是否添加mShadow

// 🌟 二次开发新增变量:标识拖动时是否有变暗效果(即是否添加 mShadow 遮罩)
// 默认设为 true 保持原版效果。如果你希望彻底取消变暗,在业务层设为 false 即可。
private boolean shadowEffectEnabled = true;

// 🌟 新增变量的 Getter 方法
public boolean isShadowEffectEnabled() {
    return shadowEffectEnabled;
}

// 🌟 新增变量的 Setter 方法
public void setShadowEffectEnabled(boolean shadowEffectEnabled) {
    this.shadowEffectEnabled = shadowEffectEnabled;
}

同样Builder中增加函数进行传递控制

 /**
     * 🌟 3. 新增链式函数:控制拖动图片放大时,是否有周围变暗的阴影效果
     * @param enabled true 代表开启变暗遮罩;false 代表彻底关闭变暗,周围 16、32 文本清晰可见。
     * @return Builder 返回自身
     */
    public Builder shadowEffectEnabled(boolean enabled) {
        this.mConfig.setShadowEffectEnabled(enabled);
        return this;
    }

startZoomingView函数中手势时间交给mZoomableView处理,怎么做?

// 🌟 1. 修改 onTouch 方法,实现后续手势的持续转发
@Override
public boolean onTouch(View v, MotionEvent event) {
// 检测到双指按下,准备启动克隆
if (!mIsZooming && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN && event.getPointerCount() >= 2) {
mIsZooming = true;
// 🌟 核心点:把当前这一帧的 event 传进去
startZoomingView(v, event);
return true;
}

// 🌟 2. 核心点:一旦进入缩放状态,后续所有的拖拽、双击、捏合手势,全部强行转发给顶层的 PhotoView
if (mIsZooming && mZoomableView != null) {
    
    // 🛠️ 核心二开留白:由于开启了松手不回弹 (autoResetOnRelease = false)
    // 用户最后一根手指抬起时,我们拦截并触发我们想要的退出逻辑(比如双击恢复,而不是一松手就回弹)
    int action = event.getActionMasked();
    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        if (mConfig.isAutoResetOnRelease()) {
            endZoomingView(); // 走原版回弹
        } else {
            // 松手不回弹!此时不呼叫 endZoomingView()。
            // 允许用户继续在 PhotoView 内部单指拖动、查看 234x300dp 证件照的细节。
            // 我们在这把事件也转发过去,让 PhotoView 记住松手后的位置。
            mZoomableView.dispatchTouchEvent(event);
            
            // 🌟 释放父布局锁定:松手了,把整个页面的大滚动控制权还给系统
            disableParentTouch(mTarget.getParent(), false); 
            return true;
        }
    }

    // 持续转发 ACTION_MOVE 等事件给 PhotoView
    return mZoomableView.dispatchTouchEvent(event);
}

return false;

}

// 🌟 3. 重构你的核心 startZoomingView 函数
private void startZoomingView(View view, MotionEvent initialEvent) {
if (mTargetContainer instanceof ViewGroupContainer) {
mZoomableView = new PhotoView(mTarget.getContext());
} else {
mZoomableView = new ImageView(mTarget.getContext());
}
mZoomableView.setLayoutParams(new ViewGroup.LayoutParams(mTarget.getWidth(), mTarget.getHeight()));
mZoomableView.setImageBitmap(ViewUtils.getBitmapFromView(view));

// show the view in the same coords
mTargetViewCords = ViewUtils.getViewAbsoluteCords(view);

mZoomableView.setX(mTargetViewCords.x);
mZoomableView.setY(mTargetViewCords.y);

if (mConfig.isShadowEffectEnabled()) {
    if (mShadow == null) {
        mShadow = new View(mTarget.getContext());
    }
    mShadow.setBackgroundResource(0);
    addToDecorView(mShadow);
}

addToDecorView(mZoomableView);

// trick for simulating the view is getting out of his parent
disableParentTouch(mTarget.getParent(), true); // 传入 true 锁定父布局

// 🛠️ 核心修正:不要用 INVISIBLE!用 Alpha 隐形占位,彻底干掉外层 ConstraintLayout 造成的 16、32 文本抖动
mTarget.setAlpha(0f); 

if (mConfig.isImmersiveModeEnabled()) {
    hideSystemUI();
}

// 🌟🌟🌟 最关键的一步:手势无缝衔接 🌟🌟🌟
// 此时原图触发了双指按下,但新生的 mZoomableView 刚生出来,错过了 ACTION_DOWN 和 POINTER_DOWN 事件。
// 我们必须手动的把触发这一瞬间的 initialEvent 强行“喂给” mZoomableView,
// 否则手势会发生断层,用户必须松开手重新按一次才能开始缩放。
if (mZoomableView != null) {
    mZoomableView.dispatchTouchEvent(initialEvent);
}

// 🛠️ 核心二开留白:为不回弹模式增加【双击图片】或【点击图片】缩回卡片的后门
if (!mConfig.isAutoResetOnRelease() && mZoomableView instanceof PhotoView) {
    ((PhotoView) mZoomableView).setOnPhotoTapListener((photoView, x, y) -> {
        // 当用户点击已经放大的图片时,手动呼叫原本的还原动画,让它飞回卡片里
        endZoomingView(); 
    });
}

if (mZoomListener != null) {
    mZoomListener.onViewStartedZooming(mTarget);
}

}
onTouch(View v, MotionEvent ev)双指按下和单手指按下如何区分?

@Override
public boolean onTouch(View v, MotionEvent ev) {
// 🌟 获取当前的动作类型(必须用 getActionMasked,不能用 getAction)
int action = ev.getActionMasked();
// 🌟 获取当前屏幕上的手指总数
int pointerCount = ev.getPointerCount();

switch (action) {
    case MotionEvent.ACTION_DOWN:
        // ☝️ 【单指按下】
        // 触发时机:屏幕上本来没有任何手指,第一根手指刚碰触屏幕的一瞬间。
        // 此时 pointerCount 必然等于 1。
        Log.d("Touch", "第一根手指按下(单指)");
        break;

    case MotionEvent.ACTION_POINTER_DOWN:
        // ✌️ 【双指 / 多指按下】
        // 触发时机:屏幕上已经有手指按住了(比如单指按住),此时第二根或更多手指刚碰触屏幕的一瞬间。
        // 此时可以通过判断 pointerCount 来精确捕获双指状态。
        if (pointerCount == 2) {
            Log.d("Touch", "第二根手指按下(现在是双指了!)");
            // 🛠️ 这就是你的 ScopeView 库触发克隆和放大(startZoomingView)的最佳契机!
        }
        break;

    case MotionEvent.ACTION_MOVE:
        // 🏃 【手指在屏幕上滑动、拖拽】
        if (pointerCount == 1) {
            // 单指在拖动(比如普通滑动列表)
        } else if (pointerCount == 2) {
            // 双指在捏合、张开或者双指平移
        }
        break;

    case MotionEvent.ACTION_POINTER_UP:
        // ✋ 【有一根手指抬起了,但屏幕上还留有其他手指】
        Log.d("Touch", "多指中的某一根手指抬起了");
        break;

    case MotionEvent.ACTION_UP:
        // 🛑 【最后一根手指也抬起了】
        // 触发时机:屏幕上仅剩的最后一根手指离开屏幕,此时屏幕恢复完全无触摸状态。
        Log.d("Touch", "最后一根手指抬起,触摸彻底结束");
        break;
}
return true;

}
onTouch(View v, MotionEvent ev)函数中,会有双指同时按下的情况吗?
会有,但在底层的毫秒级(ms)时间层面上,Android 系统依然会把它们拆解为两步分发。许多开发者在真机测试时会觉得:我的两根手指明明是“同一时间、整整齐齐”按在屏幕上的。但实际上,由于人类手指肌肉的微观差异和手机屏幕高采样率(通常是 60Hz - 240Hz,即每 4 到 16 毫秒扫描一次),在 Android 系统的触摸事件流中,绝对没有真正意义上的“同时发生”。
在 Android 系统的触摸事件流中,绝对没有真正意义上的“同时发生”。
onTouch(View v, MotionEvent ev)函数思路设计:
1,当手触摸目标图片时,第一个手指按下的时候,创建PhotoView;
2,当两指同时按下的时候,将手势事件完全交给PhotoView;
3,当PhotoView双击或者拖动还原的初始大小的时候回滚到初始状态;
分析:如果在第一个手指按下(ACTION_DOWN)时就立即创建 PhotoView 并让原图透明,用户其实还在执行单指触摸。此时如果用户只是想单指滑动翻页,或者只是点按一下,就会误触发新库的加载,导致严重的交互误触。

高阶解决方案思路:
在第1步(单指按下)时,在内存中静默创建并初始化好 PhotoView,使其在顶层处于隐形蓄势状态(setAlpha(0f)),此时不要把原图隐藏,让事件继续正常流动。一旦捕捉到第2步(双指按下),瞬间将顶层 PhotoView 显示,把原图透明,并将从 ACTION_DOWN 开始攒下来的完整历史手势流一键转发过去。

TargetContainer接口中增加函数判断,当前传入的TargetContainer实现类是否ViewGruop容器类型?

public interface TargetContainer {

ViewGroup getDecorView();

/**
 * 🌟 二次开发新增:用于标识和判断当前传入的 TargetContainer 实现类是否为 ViewGroup 容器类型
 *
 * @return true 代表是标准的 ViewGroup 容器(可以包裹子 View,适合装载 PhotoView);
 * false 代表是普通非 ViewGroup 类型。
 */
default boolean isViewGroupContainer() {
    return this instanceof ViewGroupContainer;
}

}

拖动图片完全没有反应?
给自定义的ImageView定义一个接口回调,这个回调是用于当它设置完图片并且测量完成后,能够准确获取它的宽高的场景:

// 💡 定义用于回传准确宽高的接口
public interface OnImageMeasuredListener {
    /**
     * 当图片设置完毕且 View 测量完成后触发
     * @param view 当前 ImageView 实例
     * @param width 准确的物理宽度 (px)
     * @param height 准确的物理高度 (px)
     */
    void onImageMeasured(IdPhotoImageView view, int width, int height);
}

private OnImageMeasuredListener mImageMeasuredListener;

当可以拿到ImageView的宽高的时候,我们需要让ImageView元神出窍,变身出来一个新的View,并且挂载到指定的ViewGroup上面。同时变身出来的这个新View更强大能双击放大和拖动放大。
那么挂载到指定的ViewGroup上面这个行为,我们放到一个类里实现还是放大一个方法里面?

变身出来的View需要对外暴露,由外部决定View的类型,通过Builder对外暴露:

ConstraintLayout中添加View,能直接用addview吗?
不能,需要指定布局参数才能正确的添加View。

通过屏幕坐标的方式,添加View之后,整条的位置y方向相对原来的View偏下了一些,没有完全折叠住。原因分析:目前是将 AvatarView 添加到了布局指定的ConstraintLayout中,发生的便宜显现。PhotoBrowser,照片浏览器。如何给View在代码生成一个唯一的ID?

mAvatarView.setId(View.generateViewId());
如何再代码中有效防止View被父容器多次添加addView?

private void addToDecorView(View v) {
    ViewGroup parent = mTargetContainer.getDecorView();

    if (parent == null || v == null) {
        return;
    }

    if (v.getParent() instanceof ViewGroup currentParent) {
        if (currentParent == parent) {
            return;
        }
        currentParent.removeView(v);
    }

    parent.addView(v);
}

如果View已经被添加到ViewGroup上,那么如何通过代码更新它的位置和宽高等?
当一个 View 已经被添加到 ViewGroup 上后,绝对不要再次调用 addView(),而是应该通过修改它的 LayoutParams(布局参数) 或利用系统的 ConstraintSet(约束集合) 来动态更新它的位置和宽高。
如果View已经被添加到ViewGroup上,能否继续通过代码更新它的x和y坐标?

PhotoBrowserConfig类给我增加一个变量,true的时候,和原始的ImageView公用一个Bitmap,fase的时候克隆新的。

private boolean cloneBitmap;

public boolean isCloneBitmap() {
    return cloneBitmap;
}

public void setCloneBitmap(boolean cloneBitmap) {
    this.cloneBitmap = cloneBitmap;
}

新的应用场景:原始的ImageView隐藏的时候,克隆的View也需要隐藏,那么如何根据原始ImageView的显示和隐藏的进行监听并更新新的View的显示状态呢?
要实现克隆视图(AvatarView)与原始视图(Target ImageView)的显示状态同步,最关键的是如何实时监听原始视图的可见性变化。
如果你对原始 ImageView 有控制权(即它是你自定义的类),可以重写其 onVisibilityChanged 方法,增加对外的回调。

onVisibilityChanged
如果我重写自定义的原始的ImageView的onVisibilityChanged 函数,那么我如何把它的变化状态回调到我写的克隆View里面呢?

public interface OnVisibilityChangeListener {
    void onVisibilityChanged(View changedView, int visibility);
}


public void setOnVisibilityChangeListener(OnVisibilityChangeListener listener) {
    this.mListener = listener;
}

@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
    super.onVisibilityChanged(changedView, visibility);
    if (mListener != null) {
        mListener.onVisibilityChanged(changedView, visibility);
    }
}

需求场景:原始的ImageView,被我克隆后,克隆后的View将代替原始View的一切显示和隐藏行为,同时当克隆的View显示的时候,原始的View应该是影藏的,那么如何处理刚才提出的跟随原始的View状态变化的情况呢?
1,原始的ImageView设置为可见的时候,克隆的View需要成为真正可见的,原始的ImageView实际是不可见的。
2,当原始的ImageView设置为不可见的时候,两者一起都隐藏不可见。
这种“元神出窍”后的影子跟随需求,逻辑的核心在于:原始 ImageView 变成了一个“状态开关”,而克隆 View 是“真实表现”。
接管原始 View 的可见性含义:
防止递归死循环:
通过改变View的透明度来改变可见性和通过setVisibility方式改变可见行两者的区别?
在你的“元神出窍”需求中,最佳实践是使用 setAlpha(0f),原因如下:
防止抖动:你的证件照位于 ConstraintLayout 中,侧边和底部有测量文字。使用 setAlpha(0f) 能确保这些文字的基准线(Anchor)永远不动。
坐标对齐:因为原 View 还在原地,你可以随时通过 getLocationInWindow 拿到准确的坐标来校准分身。如果用了 GONE,坐标就变 0 了。

那么如何用更好的方式来接管原始View的可见性呢?
引入代理模式(Proxy Pattern)通过在访问者和被访问对象中间加入一个间接层,可以有效隔离实现细节并控制访。
如果说现有的业务已经有大量代码在调用mTarget.setVisibility()了,我不想批量修改这些代码的情况下,能把可见性事件接管过来吗?
为了确保 mTarget 依然不可见(实现“隐身占位”),可以自定义 mTarget 的 View 并重写 setVisibility 方法。即使外部调用 mTarget.setVisibility(View.VISIBLE),内部也可以将其转为 INVISIBLE。
ImageView的Bitmap变化或者可见性变化,我们需要回调接管自己的类库,那么起一个什么样的接口名称呢?
通过原始的View获取的view.getDrawable()后,设置克隆的view,图片向上偏移了一些,是什么原因呢?
hideSystemUI();函数执行导致的。

    private void deploy() {
        if (null == mViewFactory) {
            return;
        }
        mAvatarView = mViewFactory.create(mTarget.getContext());
        if (mAvatarView == null) {
            return;
        }
        mAvatarView.setId(View.generateViewId());
        if (mAvatarView instanceof ImageView imageView) {
            if (mConfig.isCloneBitmap()) {
                imageView.setImageBitmap(PhotoBrowserUtils.getBitmapFromView(mTarget));
            } else {
                setAvatarBitmap();
            }
        }

        mAvatarView.setLayoutParams(new ViewGroup.LayoutParams(mTarget.getWidth(), mTarget.getHeight()));
        Point mTargetViewCords = PhotoBrowserUtils.getViewRelativeCords(mTarget, mTargetContainer);
        mAvatarView.setX(mTargetViewCords.x);
        mAvatarView.setY(mTargetViewCords.y);

        addToDecorView(mAvatarView);

        if (mConfig.isImmersiveModeEnabled()) {
            hideSystemUI();
        }
    }

在调用的地方进行初始化操作:

    private void onInitInchPhotoBrowser() {
        mPhotoBrowser = new PhotoBrowser.Builder(mBinding.previewRootLayout)
                .shadowEffectEnabled(false)
                .enableImmersiveMode(false)
                .cloneBitmap(false)
                .avatarFactory(context -> createPhotoView(this))
                .target(mBinding.imagePreview)
                .create();
    }

    private View createPhotoView(Context context) {
        PhotoView photoView = new PhotoView(context);
        return photoView;
    }
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容