@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;
}