LongScreenShotTile.handleClick
@Override
protected void handleClick() {
if (ActivityManager.isUserAMonkey()) {
return;
}
MetricsLogger.action(mContext, getMetricsCategory(), !mState.value);
setIsInLongshot(1);
mHost.collapsePanels();
mHandler.removeCallbacks(mScreenshotRunnable);
mHandler.postDelayed(mScreenshotRunnable, 500);
}
mScreenshotRunnable,调用takeScreenshot,关注参数WindowManager.TAKE_LONG_SCREENSHOT_SELECTED_REGION
final Runnable mScreenshotRunnable = new Runnable() {
@Override
public void run() {
takeScreenshot(WindowManager.TAKE_LONG_SCREENSHOT_SELECTED_REGION);
}
};
// Assume this is called from the Handler thread.
private void takeScreenshot(final int screenshotType) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
mContext.unbindService(mScreenshotConnection);
mScreenshotConnection = null;
}
final ComponentName serviceComponent = new ComponentName(SYSUI_PACKAGE,
SYSUI_SCREENSHOT_SERVICE);
final Intent serviceIntent = new Intent();
serviceIntent.setComponent(serviceComponent);
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != this) {
return;
}
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, screenshotType);
final ServiceConnection myConn = this;
Handler h = new Handler(mHandler.getLooper()) {
@Override
public void handleMessage(Message msg) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection == myConn) {
mContext.unbindService(mScreenshotConnection);
mScreenshotConnection = null;
mHandler.removeCallbacks(mScreenshotTimeout);
}
}
}
};
msg.replyTo = new Messenger(h);
msg.arg1 = msg.arg2 = 0;
try {
messenger.send(msg);
} catch (RemoteException e) {
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
notifyScreenshotError();
}
};
if (mContext.bindServiceAsUser(serviceIntent, conn,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
UserHandle.CURRENT)) {
mScreenshotConnection = conn;
//mHandler.postDelayed(mScreenshotTimeout, 25000);
}
}
}
流程转到TakeScreenshotService.java中
case WindowManager.TAKE_LONG_SCREENSHOT_SELECTED_REGION:
if (LONG_SCREENSHOT_ENABLED) {
mScreenshot.takeRegionScreenshot(finisher);
} else {
Toast.makeText(TakeScreenshotService.this, "long screenshot disabled", Toast.LENGTH_SHORT).show();
}
break;
流程转到mScreenshot,即GlobalScreenshot.java,该类为整个流程的重点类
长截图实际样式图
观察上图布局,是由两个layout组成,黑色背景和图片是一个layout,上面的三个按钮和底部start long screen shot是一个布局。
//GlobalScreenShot的构造方法。主要就是初始化layout及findViewById,以及设置WindowManager.LayoutParams
public GlobalScreenshot(Context context) {
Resources r = context.getResources();
mContext = context;
mResolver = mContext.getContentResolver();
LayoutInflater layoutInflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Inflate the screenshot layout
mDisplayMatrix = new Matrix();
mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
mMoreActionLayout = layoutInflater.inflate(R.layout.global_screenshot_more_action, null);
btnScrollMode = (Button) mMoreActionLayout.findViewById(R.id.btn_scroll_mode);
btnScrollMode.setVisibility(View.INVISIBLE);
//btnScrollMode.setText(mContext.getString(R.string.start_long_screen_shot));
mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background);
mScreenshotView = (ScreenShotImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
//mScreenshotView.setLimitedHeight(r.getDisplayMetrics().heightPixels * 2);
scrollView = (ScrollView) mScreenshotLayout.findViewById(R.id.scrollview);
bottomFrame = (FrameLayout) mScreenshotLayout.findViewById(R.id.screenshotview_bottom);
mSecondScreenshotView = (ScreenShotImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_bottom);
mSecondScreenshotView.setVisibility(View.GONE);
mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
mScreenshotSelectorView = (ScreenshotSelectorView) mMoreActionLayout.findViewById(R.id.screen_shot_select_view);
mScreenshotLayout.setFocusable(true);
mScreenshotSelectorView.setFocusable(true);
mScreenshotSelectorView.setFocusableInTouchMode(true);
mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// Intercept and ignore all touch events
return true;
}
});
mMoreActionLayout.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
@Override
public void onViewDetachedFromWindow(View v) {
setIsInLongshot(0);
}
@Override
public void onViewAttachedToWindow(View v) {
float degrees = getDegreesForRotation(mDisplay.getRotation());
if (degrees > 0) {
stopScreenshot();
setIsInLongshot(0);
if(mFinisher != null) {
mFinisher.run();
}
Toast.makeText(mContext, R.string.screen_shot_landscape_tips, Toast.LENGTH_SHORT).show();
}
}
});
// Inflate the partial screenshot layout
mResolver = mContext.getContentResolver();
mBitmapResultList = new ArrayList<Bitmap>();
// Setup the window that we are going to use
mWindowLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
WindowManager.LayoutParams.TYPE_SCREENSHOT,
WindowManager.LayoutParams.FLAG_FULLSCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
PixelFormat.TRANSLUCENT);
mWindowLayoutParams.setTitle("ScreenshotAnimation");
mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mNotificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
mDisplay = mWindowManager.getDefaultDisplay();
mDisplayMetrics = new DisplayMetrics();
mDisplay.getRealMetrics(mDisplayMetrics);
// Get the various target sizes
mNotificationIconSize =
r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
// Scale has to account for both sides of the bg
mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding);
mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels;
// determine the optimal preview size
int panelWidth = 0;
try {
panelWidth = r.getDimensionPixelSize(R.dimen.notification_panel_width);
} catch (Resources.NotFoundException e) {
}
if (panelWidth <= 0) {
// includes notification_panel_width==match_parent (-1)
panelWidth = mDisplayMetrics.widthPixels;
}
mPreviewWidth = panelWidth;
mPreviewHeight = r.getDimensionPixelSize(R.dimen.notification_max_height);
// Setup the Camera shutter sound
mCameraSound = new MediaActionSound();
mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
/* SPRD:Bug 900116 @{*/
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
/* @} */
}
然后来看调过来的takeRegionScreenshot(),该方法的关键地方为
takeScreenshotNoAnimation(finisher, 0, 0, mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels);
void takeRegionScreenshot(Runnable finisher) {
if(isInLongscreenshot()) {
Log.d(TAG, "current aready in mode!" + isInLongscreenshot());
return;
}
setIsInLongshot(1);
scrollView.setVisibility(View.VISIBLE);
mFinisher = finisher;
Log.d(TAG, "takeRegionScreenshot");
float degrees = getDegreesForRotation(mDisplay.getRotation());
String checkRes = null;
if (degrees > 0) checkRes = mContext.getString(R.string.screen_shot_landscape_tips);
if (!TextUtils.isEmpty(checkRes)) {
Toast info = Toast.makeText(mContext, checkRes, Toast.LENGTH_SHORT);
info.show();
finisher.run();
setIsInLongshot(0);
return;
}
mDisplay.getRealMetrics(mDisplayMetrics);
takeScreenshotNoAnimation(finisher, 0, 0, mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels);
Log.d(TAG, "(int) mDisplay.getWidth():" + (int) mDisplay.getWidth() + " (int) mDisplay.getHeight():" + (int) mDisplay.getHeight());
mScreenshotSelectorView = (ScreenshotSelectorView) mMoreActionLayout.findViewById(R.id.screen_shot_select_view);
mScreenshotSelectorView.setCurrentForLongScreenShot(true);
mScreenshotSelectorView.setFocusable(true);
mScreenshotSelectorView.setFocusableInTouchMode(true);
mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ScreenshotSelectorView view = (ScreenshotSelectorView) v;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
view.updateCurrentSelectionCircle((int) event.getX(), (int) event.getY());
return true;
case MotionEvent.ACTION_MOVE:
view.updateRegionSelection((int) event.getX(), (int) event.getY());
return true;
case MotionEvent.ACTION_UP:
return true;
}
return false;
}
});
mScreenshotLayout.post(new Runnable() {
@Override
public void run() {
mScreenshotSelectorView.setVisibility(View.VISIBLE);
mScreenshotSelectorView.requestFocus();
mSecondScreenshotView.setVisibility(View.GONE);
}
});
}
在来看下takeScreenshotNoAnimation(),该方法首先调用native方法SurfaceControl.screenshot,截图。然后通过WindowManager.addView将之前加载的显示截图的layout添加进来。此处还有一个createScreenshotDropInAnimation,截图看到是将截到的图缩小了显示出来的,这是通过DropIn来做的。
/**
* Takes a screenshot of the current display and shows an animation.
*/
void takeScreenshotNoAnimation(Runnable finisher, int x, int y, int width, int height) {
// We need to orient the screenshot correctly (and the Surface api seems to take screenshots
// only in the natural orientation of the device :!)
mDisplay.getRealMetrics(mDisplayMetrics);
float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
float degrees = getDegreesForRotation(mDisplay.getRotation());
boolean requiresRotation = (degrees > 0);
if (requiresRotation) {
// Get the dimensions of the device in its native orientation
mDisplayMatrix.reset();
mDisplayMatrix.preRotate(-degrees);
mDisplayMatrix.mapPoints(dims);
dims[0] = Math.abs(dims[0]);
dims[1] = Math.abs(dims[1]);
}
int rot = mDisplay.getRotation();
Rect sourceCrop = new Rect(0, 0, (int) dims[0], (int) dims[1]);
// Take the screenshot
mScreenBitmap = SurfaceControl.screenshot(sourceCrop, (int) dims[0], (int) dims[1], rot);
Log.d(TAG, "takeScreenshotNoAnimation");
if (mScreenBitmap == null) {
notifyScreenshotError(mContext, mNotificationManager,
R.string.screenshot_failed_to_capture_text);
finisher.run();
setIsInLongshot(0);
Log.d(TAG, "takeScreenshotNoAnimation return");
return;
}
if (requiresRotation) {
// Rotate the screenshot to the current orientation
Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(ss);
c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
c.rotate(degrees);
c.translate(-dims[0] / 2, -dims[1] / 2);
c.drawBitmap(mScreenBitmap, 0, 0, null);
c.setBitmap(null);
// Recycle the previous bitmap
mScreenBitmap.recycle();
mScreenBitmap = ss;
}
if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) {
/*SPRD bug 670330:Catch IllegalArgumentException*/
try {
// Crop the screenshot to selected region
Bitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height);
mScreenBitmap.recycle();
mScreenBitmap = cropped;
} catch (IllegalArgumentException e) {
// TODO: handle exception
e.printStackTrace();
notifyScreenshotError(mContext, mNotificationManager,
R.string.screenshot_failed_to_save_unknown_text);
finisher.run();
setIsInLongshot(0);
mScreenBitmap.recycle();
mScreenBitmap = null;
return;
}
/*@}*/
}
// Optimizations
mScreenBitmap.setHasAlpha(false);
mScreenBitmap.prepareToDraw();
mScreenshotView.setImageBitmap(mScreenBitmap);
mScreenshotLayout.requestFocus();
// Setup the animation with the screenshot just taken
if (mScreenshotAnimation != null) {
if (mScreenshotAnimation.isStarted()) {
mScreenshotAnimation.end();
}
mScreenshotAnimation.removeAllListeners();
}
if(mScreenshotLayout.getParent() != null) {
mWindowManager.removeView(mScreenshotLayout);
}
mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
mScreenshotAnimation = new AnimatorSet();
mScreenshotAnimation.play(screenshotDropInAnim);
mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
float degrees = getDegreesForRotation(mDisplay.getRotation());
if (degrees > 0) {
if(mScreenshotLayout.getParent() != null) {
mWindowManager.removeView(mScreenshotLayout);
}
setIsInLongshot(0);
if(mFinisher != null) {
mFinisher.run();
}
Toast.makeText(mContext, R.string.screen_shot_landscape_tips, Toast.LENGTH_SHORT).show();
return;
}
int[] location = new int[2];
mScreenshotView.getLocationOnScreen(location);
if(mMoreActionLayout.getParent() != null) {
mWindowManager.removeView(mMoreActionLayout);
}
mWindowManager.addView(mMoreActionLayout, mWindowLayoutParams);
mScreenshotSelectorView.startRegionSelection((int) dims[0], (int) dims[1], location);
initButtonActions(finisher);
resetDate();
sendLongscreenshotBroadcast();
mScreenshotLayout.postDelayed(new Runnable() {
@Override
public void run() {
if (btnScrollMode.getVisibility() != View.VISIBLE) {
setSupportLongScreenshot(supportLongScreenshot);
}
}
}, 500);
}
});
mScreenshotLayout.post(new Runnable() {
@Override
public void run() {
mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mScreenshotView.buildLayer();
mScreenshotAnimation.start();
}
});
}
private ValueAnimator createScreenshotDropInAnimation() {
final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION)
/ SCREENSHOT_DROP_IN_DURATION);
final float flashDurationPct = 2f * flashPeakDurationPct;
final Interpolator flashAlphaInterpolator = new Interpolator() {
@Override
public float getInterpolation(float x) {
// Flash the flash view in and out quickly
if (x <= flashDurationPct) {
return (float) Math.sin(Math.PI * (x / flashDurationPct));
}
return 0;
}
};
final Interpolator scaleInterpolator = new Interpolator() {
@Override
public float getInterpolation(float x) {
// We start scaling when the flash is at it's peak
if (x < flashPeakDurationPct) {
return 0;
}
return (x - flashDurationPct) / (1f - flashDurationPct);
}
};
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(SCREENSHOT_DROP_IN_DURATION);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mBackgroundView.setAlpha(0f);
mBackgroundView.setVisibility(View.VISIBLE);
scrollView.setAlpha(0f);
scrollView.setTranslationX(0f);
scrollView.setTranslationY(0f);
scrollView.setScaleX(SCREENSHOT_SCALE + mBgPaddingScale);
scrollView.setScaleY(SCREENSHOT_SCALE + mBgPaddingScale);
mScreenshotView.setVisibility(View.VISIBLE);
mScreenshotFlash.setAlpha(0f);
mScreenshotFlash.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(android.animation.Animator animation) {
//mScreenshotFlash.setVisibility(View.GONE);
}
});
anim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float t = (Float) animation.getAnimatedValue();
float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale)
- scaleInterpolator.getInterpolation(t)
* (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE);
mBackgroundView.setAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA);
scrollView.setAlpha(t);
scrollView.setScaleX(scaleT);
scrollView.setScaleY(scaleT);
mScreenshotFlash.setAlpha(flashAlphaInterpolator.getInterpolation(t));
}
});
return anim;
}
在这一系列动画结束之后,又调用关键方法
initButtonActions(finisher);
resetDate();
sendLongscreenshotBroadcast();
initButtonActions方法就是在初始化mMoreActionLayout,即截图布局中上面三个cancel、share、save和下面的start long screenshot这些按钮的初始化及事件处理。
private void initButtonActions(final Runnable finisher) {
Button btnCancel = (Button) mMoreActionLayout.findViewById(R.id.btn_cancel);
Button btnShare = (Button) mMoreActionLayout.findViewById(R.id.btn_share);
Button btnSave = (Button) mMoreActionLayout.findViewById(R.id.btn_save);
mMoreActionLayout.findViewById(R.id.more_action_layout).setVisibility(View.VISIBLE);
btnCancel.setVisibility(View.VISIBLE);
btnCancel.setText(mContext.getString(com.android.internal.R.string.cancel));
btnSave.setVisibility(View.VISIBLE);
btnSave.setText(mContext.getString(R.string.save));
btnShare.setText(mContext.getString(com.android.internal.R.string.share));
KeyguardManager keyguardManager =
(KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager.isKeyguardLocked()) {
btnShare.setVisibility(View.INVISIBLE);
} else {
btnShare.setVisibility(View.VISIBLE);
}
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stopLongscreenshot();
stopScreenshot();
finisher.run();
}
});
btnShare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
shareScreenshot(finisher);
setIsInLongshot(0);
}
});
btnSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveScreenshot(finisher);
}
});
btnScrollMode.setText("");
btnScrollMode.setEnabled(false);
btnScrollMode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isInLongscreenshot) {
v.setEnabled(false);
startLongscreenshot();
((Button)v).setText(mContext.getString(R.string.stop_long_screen_shot));
mMoreActionLayout.findViewById(R.id.more_action_layout).setVisibility(View.GONE);
mScreenshotSelectorView.setVisibility(View.GONE);
mSecondScreenshotView.setVisibility(View.GONE);
float s = scrollView.getScaleX();
bottomFrame.setScaleX(s);
bottomFrame.setScaleY(s);
} else {
btnScrollMode.setEnabled(false);
btnScrollMode.setText("stopping...");
stopLongBitmapAnimation();
mMoreActionLayout.postDelayed(new Runnable() {
@Override
public void run() {
if (isInLongscreenshot) {
saveAndShowLongScreenShot();
stopLongscreenshot();
stopScreenshot();
}
}
}, STOP_LONGSCREENSHOT_TIMEOUT);
}
}
});
}
稍后我们要关注下开始截图按钮,即btnScrollMode的事件处理。现在往下走还有sendLongscreenshotBroadcast()关键方法。
void sendLongscreenshotBroadcast() {
ActivityManager mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> tasks = mActivityManager.getRunningTasks(1);
ComponentName cmp = tasks.get(0).topActivity;
SaveImageInBackgroundTask.getScreenshotPicInfo(mScreenshotPicInfo);
String packageName = cmp.getPackageName();
String activityName = cmp.getClassName();
Intent intent = new Intent("action.longscreenshot");
intent.putExtra("package", packageName);
intent.putExtra("activity", activityName);
intent.putExtra("filepath", mScreenshotPicInfo.longscreenshotFilePath);
mContext.sendBroadcast(intent);
Log.d(TAG, "sendLongscreenshotBroadcast: packageName=" + packageName + " activityName=" + activityName);
}
我们看到这个方法就是发送了一个action为action.longscreenshot的广播,并携带了相关参数。那个现在的问题就是,谁接受了这个广播???
关键点来了,在Actvity里面处理的。
这里要说下长截图的思路了。长截图肯定是免不了对可滚动View的处理了,展讯这里的思路就是穷尽法,处理我们认为可以滚动的view。在Activity的onResume中注册监听这个广播。
Activity接收到长截图广播后的处理,new了一个 LongScreenshotUtil,从名字上看就是一个长截图的工具类。也是Actvity里面处理长截图的。
new完之后就开始bindService。和SystemUI里面的TakeScreenshotService建立联系。
z
在onServiceConnected中有个关键方法updateSupportState()
即getScrollableView()决定当前界面是否能够支持长截图
然后我们来看下什么样的view才叫isScrollableView
/**
* judge whether a decor contains a scrollable view
*/
private boolean isScrollableView(ViewGroup viewGroup) {
boolean result = false;
if (viewGroup instanceof ScrollView) {
Log.d(TAG, "viewGroup is ScrollView");
ScrollView scrollView = (ScrollView) viewGroup;
View child = scrollView.getChildAt(0);
//whether scrollView scroll to bottom
if (child.getMeasuredHeight() <= scrollView.getScrollY() + scrollView.getHeight()) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "ScrollView has reached bottom");
} else {
result = true;
scrollType = SCROLLVIEW;
}
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "child.getMeasuredHeight() = " + child.getMeasuredHeight());
return result;
} else if (viewGroup instanceof AbsListView) {
Log.d(TAG, "viewGroup is ListView");
AbsListView listView = (AbsListView) viewGroup;
int count = listView.getCount();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "count = " + count + " getChildCount = " + listView.getChildCount());
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "LastVisiblePosition = " + listView.getLastVisiblePosition());
if (count > 0) result = true;
//whether listview scroll to bottom
if (listView.getLastVisiblePosition() >= count - 1 && count > 0) {
int lastItemBottom = listView.getChildAt(listView.getChildCount() - 1).getBottom();
int listViewheight = listView.getHeight();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "lastItemBottom = " + lastItemBottom);
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "listView.getHeight() = " + listViewheight);
if (lastItemBottom <= listViewheight) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "ListView has reached bottom");
}
}
if (result) scrollType = LISTVIEW;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "count = " + count + " LastVisiblePosition = " + listView.getLastVisiblePosition());
return result;
} else if (viewGroup instanceof WebView) {
Log.d(TAG, "viewGroup is WebView");
WebView webView = (WebView) viewGroup;
int verticalScrollRange = 0;
int verticalScrollExtent = 1;
int offset = 2;
Class clazz = viewGroup.getClass();
for (;;) {
String classNamepath = clazz.getSimpleName().toLowerCase();//className = com.tencent.widget.armaphongbaolistvie
if (classNamepath.equals("webview")) {
break;
}
clazz = clazz.getSuperclass();
}
try {
Method m1 = clazz.getDeclaredMethod("computeVerticalScrollRange", null);
Method m2 = clazz.getDeclaredMethod("computeVerticalScrollExtent", null);
Method m3 = clazz.getDeclaredMethod("computeVerticalScrollOffset", null);
m1.setAccessible(true);
m2.setAccessible(true);
m3.setAccessible(true);
verticalScrollRange = (Integer)m1.invoke(viewGroup);
verticalScrollExtent = (Integer)m2.invoke(viewGroup);
offset = (Integer)m3.invoke(viewGroup);
} catch (Exception e) {
e.printStackTrace();
}
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "verticalScrollRange=" + verticalScrollRange + " verticalScrollExtent=" + verticalScrollExtent + " offset=" + offset);
if(webView.getContentHeight()*webView.getScale()-(webView.getHeight()+webView.getScrollY())==0){
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "WebView ContentHeight=" + webView.getContentHeight()*webView.getScale() + " Height=" + webView.getHeight());
//webview has reached bottom
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "WebView has reached bottom");
} else {
result = true;
scrollType = WEBVIEW;
}
if(verticalScrollRange-verticalScrollExtent-offset <= 0){
//webview has reached bottom
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "WebView has reached bottom.");
} else {
result = true;
scrollType = WEBVIEW;
}
return result;
} else {
int scrollableViewIndex = checkWhetherScrollableView(viewGroup);
if (scrollableViewIndex == 1) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "viewGroup is self-defined listview");
try {
Method mGetCount = viewGroup.getClass().getMethod("getCount", null);
Method mGetLastVisiblePosition = viewGroup.getClass().getMethod("getLastVisiblePosition", null);
int count = (Integer)mGetCount.invoke(viewGroup);
int lastVisiblePosition = (Integer)mGetLastVisiblePosition.invoke(viewGroup);
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "LastVisiblePosition = " + lastVisiblePosition + " count = " + count);
if (count > 0) result = true;
//whether listview scroll to bottom
if (lastVisiblePosition >= count - 1 && count > 0) {
int lastItemBottom = viewGroup.getChildAt(viewGroup.getChildCount() - 1).getBottom();
int listViewHeight = viewGroup.getHeight();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "lastItemBottom = " + lastItemBottom);
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "listViewHeight = " + listViewHeight);
if (lastItemBottom <= listViewHeight) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "ListView has reached bottom");
}
}
if (result) scrollType = SELF_DEFINED_LISTVIEW;
//if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "count = " + count + " LastVisiblePosition = " + listView.getLastVisiblePosition());
return result;
} catch (Exception e) {
e.printStackTrace();
return false;
}
} else if (scrollableViewIndex == 2) {
try {
String className = viewGroup.getClass().getName();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "viewGroup = " + className+" viewGroup.getClass():"+viewGroup.getClass().toString());
Log.d(TAG, "viewGroup is self-RecyclerView");
Method m1 = recyclerViewClass.getDeclaredMethod("computeVerticalScrollRange", null);
Method m2 = recyclerViewClass.getDeclaredMethod("computeVerticalScrollExtent", null);
Method m3 = recyclerViewClass.getDeclaredMethod("computeVerticalScrollOffset", null);
m1.setAccessible(true);
m2.setAccessible(true);
m3.setAccessible(true);
int verticalScrollRange = (Integer)m1.invoke(viewGroup);
int verticalScrollExtent = (Integer)m2.invoke(viewGroup);
int maxScrollDistance = verticalScrollRange - verticalScrollExtent;
int currentScrollDistance = (Integer)m3.invoke(viewGroup);
Log.d(TAG, "maxScrollDistance = " + maxScrollDistance + ", currentScrollDistance = " + currentScrollDistance);
if (currentScrollDistance >= maxScrollDistance) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "RecyclerView has reached bottom");
} else {
result = true;
scrollType = RECYCLEVIEW;
}
} catch(Exception e) {
e.printStackTrace();
return false;
}
return result;
} else if (scrollableViewIndex == 3) {
try {
String className = viewGroup.getClass().getName();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "viewGroup = " + className);
Log.d(TAG, "viewGroup is self-WebView");
Method m1 = viewGroup.getClass().getMethod("getContentHeight", null);
Method m2 = viewGroup.getClass().getMethod("getScale", null);
m1.setAccessible(true);
m2.setAccessible(true);
int contentHeight = (Integer)m1.invoke(viewGroup);
float scale = (Float)m2.invoke(viewGroup);
int currentScrollDistance = viewGroup.getScrollY();
float leftScrollDistance = contentHeight * scale - viewGroup.getHeight() - currentScrollDistance;
Log.d(TAG, "leftScrollDistance = " + leftScrollDistance );
if (contentHeight * scale - viewGroup.getHeight() - currentScrollDistance <= 1) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "WebView has reached bottom");
} else {
result = true;
scrollType = SELF_WEBVIEW;
}
} catch(Exception e) {
e.printStackTrace();
return false;
}
return result;
} else if (scrollableViewIndex == 4) {
View child = viewGroup.getChildAt(0);
//whether scrollView scroll to bottom
if (child.getMeasuredHeight() <= viewGroup.getScrollY() + viewGroup.getHeight()) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "ScrollView has reached bottom");
} else {
result = true;
scrollType = SCROLLVIEW;
}
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "child.getMeasuredHeight() = " + child.getMeasuredHeight());
return result;
}
}
Log.d(TAG, "isScrollableView:result = " + result);
return result;
}
可以看到这里其实就是一个穷尽法,有ScrollView、AbsListView、WebView、还有一个else自定义的相关类。
我们这里以Settings主界面为例,我们可以用工具看到Settings主界面可滚动的部分是com.android.settings.dashboard.conditional.FocusRecyclerView,即进入上面isScrollableView的最后一个分支else判断。
这个方法其实是为自定义继承系统可滚动view准备的,这里的判断都是contains。。。并且向上找了两层父view,即当前view,父view,父父view包含listview、recyclerview、webview、scrollview就能进入下面的判断。进入下面的判断就是在分类,并且会一直clazz.getSuperclass()。
private int checkWhetherScrollableView(ViewGroup viewGroup) {
if ((!viewGroup.getClass().getName().toLowerCase().contains("listview")
&& !viewGroup.getClass().getSuperclass().getName().toLowerCase().contains("listview")
&& !viewGroup.getClass().getSuperclass().getSuperclass().getName().toLowerCase().contains("listview"))
&&(!viewGroup.getClass().getName().toLowerCase().contains("recyclerview")
&& !viewGroup.getClass().getSuperclass().getName().toLowerCase().contains("recyclerview")
&& !viewGroup.getClass().getSuperclass().getSuperclass().getName().toLowerCase().contains("recyclerview"))
&&(!viewGroup.getClass().getName().toLowerCase().contains("webview")
&& !viewGroup.getClass().getSuperclass().getName().toLowerCase().contains("webview")
&& !viewGroup.getClass().getSuperclass().getSuperclass().getName().toLowerCase().contains("webview"))
&&(!viewGroup.getClass().getName().toLowerCase().contains("scrollview")
&& !viewGroup.getClass().getSuperclass().getName().toLowerCase().contains("scrollview")
&& !viewGroup.getClass().getSuperclass().getSuperclass().getName().toLowerCase().contains("scrollview"))){
return 0;
}
int res = 0;
Class clazz = viewGroup.getClass();
for (;;) {
String classNamepath = clazz.getName().toLowerCase();//className = com.tencent.widget.armaphongbaolistvie
int index = classNamepath.lastIndexOf(".");
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "classNamepath = " + classNamepath + " index = " + classNamepath.lastIndexOf(".")+" clazz:"+clazz.toString());
String className = classNamepath.substring(index + 1);
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "className = " + className);
if (className.equals("listview")) {
res = 1;
break;
} else if (className.equals("abslistview")) {
res = 1;
break;
} else if (className.equals("viewgroup")) {
break;
} else if (className.equals("adapterview")) {
res = 1;
break;
} else if (className.equals("recyclerview")) {
res = 2;
recyclerViewClass = clazz;
if (classNamepath.equals("android.support.v7.widget.recyclerview")) isSelfRecyclerView = false;
break;
} else if (className.equals("webview")) {
res = 3;
break;
} else if (className.equals("scrollview")) {
res = 4;
break;
}
clazz = clazz.getSuperclass();
}
return res;
}
以我们本例为例,会得到:
res = 2;
isSelfRecyclerView = false;
else if (scrollableViewIndex == 2) {
try {
String className = viewGroup.getClass().getName();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "viewGroup = " + className+" viewGroup.getClass():"+viewGroup.getClass().toString());
Log.d(TAG, "viewGroup is self-RecyclerView");
Method m1 = recyclerViewClass.getDeclaredMethod("computeVerticalScrollRange", null);
Method m2 = recyclerViewClass.getDeclaredMethod("computeVerticalScrollExtent", null);
Method m3 = recyclerViewClass.getDeclaredMethod("computeVerticalScrollOffset", null);
m1.setAccessible(true);
m2.setAccessible(true);
m3.setAccessible(true);
int verticalScrollRange = (Integer)m1.invoke(viewGroup);
int verticalScrollExtent = (Integer)m2.invoke(viewGroup);
int maxScrollDistance = verticalScrollRange - verticalScrollExtent;
int currentScrollDistance = (Integer)m3.invoke(viewGroup);
Log.d(TAG, "maxScrollDistance = " + maxScrollDistance + ", currentScrollDistance = " + currentScrollDistance);
if (currentScrollDistance >= maxScrollDistance) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "RecyclerView has reached bottom");
} else {
result = true;
scrollType = RECYCLEVIEW;
}
} catch(Exception e) {
e.printStackTrace();
return false;
}
return result;
}
得到RecycleView之后会通过反射调用RecycleView的方法去获取当前滚动的距离(如果Settings主界面已经滚动到了底部,那样也是不支持长截图的),最终得出result,返回isScrollableView()的boolean类型的值。然后就一层层返回,isScrollableView()返回到getScrollableView(),在返回到isSupportLongScreenshot(),在返回到updateSupportState()。你是否忘记了我们是从哪里调过来的☺
private void updateSupportState() {
supportLongScreenshot = !isInMultiWindowMode() && isSupportLongScreenshot();
Message msg = Message.obtain(null, UPDATE_LONG_SCREENSHOT_SUPPORT_STATE,
supportLongScreenshot ? 1 : 0, 0);
sendMessageToService(msg);
}
得到这个supportLongScreenshot 值后会调用这个sendMessageToService,从名字就可以看出来是要给service发动数据了。
private void sendMessageToService(int what) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "sendMessageToService what = " + what);
Message msg = Message.obtain(null, what);
msg.replyTo = receiveMessenger;
try {
screenshotMessenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "screenshotMessenger send message fail ! what=" + what);
}
}
流程转到SystemUI下的TakScreenshotService中
case UPDATE_LONG_SCREENSHOT_SUPPORT_STATE:
replyMesenger = msg.replyTo;
boolean supportLongScreenshot = msg.arg1 == 1;
mScreenshot.setSupportLongScreenshot(supportLongScreenshot);
break;
又到GlobalScreenshot中,传过来的boolean值决定了界面上是否允许点击 start long screenshot 按钮
public void setSupportLongScreenshot(boolean supportLongScreenshot) {
this.supportLongScreenshot = supportLongScreenshot;
if (supportLongScreenshot) {
btnScrollMode.setText(mContext.getString(R.string.start_long_screen_shot));
btnScrollMode.setEnabled(true);
btnScrollMode.setVisibility(View.VISIBLE);
} else {
btnScrollMode.setText(mContext.getString(R.string.not_support_long_screen_shot));
btnScrollMode.setEnabled(false);
btnScrollMode.setVisibility(View.VISIBLE);
}
registerStopLongScreenshotReceiver();
}
假使现在是能向下滚动的,这里就是允许 长截图的。
然后我们在回到SystemUI,发送长截图的广播找到了是在Activity里面处理的,处理完之后的效果是Activity中的LongScreenshotUtils与SystemUI建立了联系。然后我们去点击 start long screenshot
protected void startLongscreenshot() {
isInLongscreenshot = true;
if (supportLongScreenshot) screenshotService.sendLongscreenshotMessage(TakeScreenshotService.START_LONG_SCREENSHOT);
btnScrollMode.setEnabled(true);
}
点击长截图,SystemUI里面没有做实际工作,只是给TakeSceenshotService发送了一个START_LONG_SCREENSHOT消息。然后TakeSceenshotService又将此消息发送给Activity
Activity下的LongScreenshotUtils的takeLongscreenshot()方法被调用,方法前面就是一些判断,当前界面是否符合支持长截图。。然后该方法底部就是关键的东西了。
这里我们看到有很多controller,这些都是在LongScreenshotUtils中的定义的内部类,有个基类
abstract class ScrollController,然后其他的controller都是继承这个基类的。
abstract class ScrollController {
//the target to scroll and capture it's long view
protected ViewGroup view;
//scroll distance of SCROLL_DISTANCE by time of SCROLL_DURATION at once
protected static final int SCROLL_DISTANCE = 500;
protected static final int SCROLL_DURATION = 50;
protected int onceScrollDistance = SCROLL_DISTANCE;
protected int maxScrollDistance = 0;
protected int leftScrollDistance = 0;
//remember scrollBar state before scrolling and resume scrollBar when scrollComplete
protected boolean scrollBarEnabled = true;
protected int scrolltimes = 0;
protected Handler scrollHandler = null;
protected int startScrollY = 0;
protected CheckScrollDistanceRunnable checkScrollDistance = new CheckScrollDistanceRunnable();
protected Runnable scrollTimeoutRunnable = new Runnable() {
public void run() {
setScrollComplete();
sendScrollMessage(0);
}
};
//use to compute last scroll distance
protected int lastItemPreBottom = 0;
protected View lastItemView = null;
public ScrollController(ViewGroup view) {
this.view = view;
scrollBarEnabled = view.isVerticalScrollBarEnabled();
//view.setVerticalScrollBarEnabled(false);
//setScrollDistanceListener();
}
public void setOnceScrollDistance(int distance) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "setOnceScrollDistance:onceScrollDistance=" + distance);
onceScrollDistance = distance;
}
protected void setScrollHandler(Handler scrollHandler) {
this.scrollHandler = scrollHandler;
}
protected void startScroll() {//(前面有过截图的操作)点击开始长截图后调用到这里,在SystemUI里面已经截图了一次
//view.setVerticalScrollBarEnabled(false);
setScrollDistanceListener();
maxScrollDistance = countMaxScrollDistance();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "startScroll:maxScrollDistance = " + maxScrollDistance);
continueScroll();
}
protected void continueScroll() {
//view.removeCallbacks(scrollTimeoutRunnable);
if (this.canScrollDown()) {
scrolltimes += 1;
startScrollY = computeCurrentScrollY();
lastItemView = view.getChildAt(view.getChildCount() - 1);
if (lastItemView != null) lastItemPreBottom = lastItemView.getBottom();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "ScrollController continueScroll: lastItemPreBottom = " + lastItemPreBottom+" scrollBy==>onceScrollDistance:"+onceScrollDistance
+" view.getChildCount():"+view.getChildCount());
view.scrollBy(0, onceScrollDistance);
//view.scrollTo(0, view.getScrollY() + onceScrollDistance);
//view.postDelayed(scrollTimeoutRunnable,300);
} else {
setScrollComplete();
}
}
protected void sendScrollMessage(int scrollDistance) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "sendScrollMessage: scrollDistance=" + scrollDistance, new Throwable());
if (scrollDistance > 0) {
Message shotMessage = scrollHandler.obtainMessage(CAPTURE_SCREEN, scrollDistance);
scrollHandler.sendMessage(shotMessage);
}
if (scrollDistance < onceScrollDistance) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "should have scroll to bottom");
Message completeMessage = scrollHandler.obtainMessage(SCROLL_COMPLETE);
scrollHandler.sendMessage(completeMessage);
}
}
protected int computeCurrentScrollY() {
return view.getScrollY();
}
protected int computeScrollDistance() {
return computeCurrentScrollY() - startScrollY;
}
protected void setScrollComplete() {
//view.removeCallbacks(scrollTimeoutRunnable);
view.setVerticalScrollBarEnabled(scrollBarEnabled);
clearScrollDistanceListener();
resetOverlayViews();
//sendScrollMessage(-1);
}
//set scroll listener which will compute scrollDistance after scroll once
protected void setScrollDistanceListener() {
view.setScrollDistanceRunnable(checkScrollDistance);
}
protected void clearScrollDistanceListener() {
view.setScrollDistanceRunnable(null);
}
protected void resetOverlayViews() {
if (shouldCheckOverlay) {
if (overlayViews != null && overlayViews.size() > 0) {
for (int i = 0; i < overlayViews.size(); i++) {
View view = overlayViews.get(i);
view.setVisibility(View.VISIBLE);
}
}
}
}
//whether scrollableView canScrollDown any more
abstract boolean canScrollDown();
abstract int countMaxScrollDistance();
protected int fitScrollDistance(int scrollDistance) {
if (canScrollDown() /*|| scrollDistance > onceScrollDistance*/) {
return onceScrollDistance;
} else {
if (lastItemView != null) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "fitScrollDistance continueScroll: lastItemPreBottom = " + lastItemView.getBottom());
return lastItemPreBottom - lastItemView.getBottom();
}
}
return scrollDistance;
}
protected class CheckScrollDistanceRunnable implements Runnable {
public CheckScrollDistanceRunnable() {
}
@Override
public void run() {
int scrollDistance = computeScrollDistance();
if (lastItemView != null) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "run: lastItemPreBottom = " + lastItemPreBottom+" lastItemView.getBottom():" +lastItemView.getBottom());
scrollDistance = lastItemPreBottom - lastItemView.getBottom();
}
if ((scrollDistance != onceScrollDistance && scrollType != RECYCLEVIEW) || scrollType == SELF_WEBVIEW){
scrollDistance = fitScrollDistance(scrollDistance);
}
//if (scrollType != RECYCLEVIEW) scrollDistance = fitScrollDistance(scrollDistance);
if (scrollDistance > onceScrollDistance) scrollDistance = onceScrollDistance;
//if (scrollType == SELF_WEBVIEW && scrollDistance > leftScrollDistance) scrollDistance = leftScrollDistance;
//if (scrollType == SCROLLVIEW) scrollDistance = fitScrollDistance(scrollDistance);
Log.d(TAG, "scrollDistance = " + scrollDistance);
sendScrollMessage(scrollDistance);
}
}
}
我们关注针对Settings主界面的RecyclerViewController
class RecyclerViewController extends ScrollController {
private Method m1, m2, m3;
private int leftMaxScrollDistance = 0;
private int preLeftMaxScrollDistance;
private boolean selfRecyclerView = false;
public RecyclerViewController(ViewGroup view, Class clazz, boolean selfRecyclerView) {
super(view);
this.selfRecyclerView = selfRecyclerView;
createMethods(clazz);
}
private void createMethods(Class clazz) {
try {
m1 = clazz.getDeclaredMethod("computeVerticalScrollRange", null);
m2 = clazz.getDeclaredMethod("computeVerticalScrollExtent", null);
m3 = clazz.getDeclaredMethod("computeVerticalScrollOffset", null);
m1.setAccessible(true);
m2.setAccessible(true);
m3.setAccessible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
protected void setScrollDistanceListener() {
if (!selfRecyclerView) super.setScrollDistanceListener();
}
protected void clearScrollDistanceListener() {
if (!selfRecyclerView) super.clearScrollDistanceListener();
}
@Override
protected int computeCurrentScrollY() {
int result = 0;
try {
result = (Integer)m3.invoke(view);
} catch (Exception e) {
e.printStackTrace();
}
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "computeStartScrollY:result = " + result + " selfRecyclerView?" + selfRecyclerView);
return result;
}
@Override
protected void continueScroll() {
Log.e(TAG,"RecyclerViewController continueScroll selfRecyclerView:"+selfRecyclerView);
if (!selfRecyclerView) {
super.continueScroll();
return;
}
if (!canScrollDown()) {
setScrollComplete();
return;
}
scrolltimes += 1;
startScrollY = computeCurrentScrollY();
lastItemView = view.getChildAt(view.getChildCount() - 1);
if (lastItemView != null) lastItemPreBottom = lastItemView.getBottom();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "RecyclerViewController continueScroll: lastItemPreBottom = " + lastItemPreBottom);
preLeftMaxScrollDistance = leftMaxScrollDistance;
view.scrollBy(0, onceScrollDistance);
view.postDelayed(checkScrollDistance, 200);
}
@Override
protected boolean canScrollDown() {
boolean result = true;
try {
int verticalScrollRange = (Integer)m1.invoke(view);
int verticalScrollExtent = (Integer)m2.invoke(view);
int maxDistance = verticalScrollRange - verticalScrollExtent;
int currentScrollDistance = (Integer)m3.invoke(view);
leftMaxScrollDistance = maxDistance - currentScrollDistance;
Log.d(TAG, "maxDistance = " + maxDistance + ", leftMaxScrollDistance = " + leftMaxScrollDistance + " scrollY = " + view.getScrollY());
if (currentScrollDistance >= maxDistance || preLeftMaxScrollDistance == leftMaxScrollDistance) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "RecyclerView has reached bottom");
}
} catch (Exception e) {
e.printStackTrace();
}
Log.d(TAG, "canScrollDown:result = " + result);
return result;
}
protected int countMaxScrollDistance() {//剩余可滚动的距离
try {
int verticalScrollRange = (Integer)m1.invoke(view);
int verticalScrollExtent = (Integer)m2.invoke(view);
int currentScrollDistance = (Integer)m3.invoke(view);
maxScrollDistance = verticalScrollRange - verticalScrollExtent;
leftMaxScrollDistance = maxScrollDistance - currentScrollDistance;
} catch (Exception e) {
e.printStackTrace();
}
return leftMaxScrollDistance;
}
protected int fitScrollDistance(int scrollDistance) {
Log.e(TAG,"RecyclerViewController fitScrollDistance");
int preLeftMaxScrollDistance = leftMaxScrollDistance;
//if (!canScrollDown() /*&& scrolltimes == 1 && scrollDistance < leftMaxScrollDistance*/) return preLeftMaxScrollDistance;
return super.fitScrollDistance(scrollDistance);
}
}
回到takeLongscreenshot(),稍后在来分析controller,takeLongscreenshot()继续往下走会调用startScroller().
private void startScroll() {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "startScroll");
scrollableView.setVerticalScrollBarEnabled(false);
Bitmap first = captureScreen(lContext, getWindow().getDecorView());//长截图会调到这里 有一次截图的操作
if(first == null) {
sendMessageToService(LONG_SCREENSHOT_UNKOWN_ERROR);
return;
}
bitmaps.add(first);
totalHeight = first.getHeight();
scrollController.startScroll();//第一次截好图后就开始scroll
}
这个方法有两个关键点
1,captureScreen
这个截图与SystemUI里面通过SurfaceControl.screenshot截图不一样,view.getDrawingCache(),这个截出来是图片大小是整个手机屏幕大小(720*1520),但是状态栏与导航栏部分是有颜色,没内容
private Bitmap captureScreen(Context context, View view) {//截得是去掉导航栏的高度(包括状态栏高度,但是状态栏的内容是没有的)
Log.e(TAG,"captureScreen view:"+view.getClass().toString());
disableOverlayViews();
view.setDrawingCacheEnabled(true);
Bitmap b1;
try {
view.buildDrawingCache();
b1 = view.getDrawingCache();
} catch (Exception e) {
Log.d(TAG, "captureScreen:" + e.getMessage());
isScreenshotError = true;
return null;
}
Rect frame = new Rect();
view.getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
screenBottom = frame.bottom;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "statusBarHeight = " + statusBarHeight + " naviBarHeight = " + screenBottom);
int width = context.getResources().getDisplayMetrics().widthPixels;
int height = context.getResources().getDisplayMetrics().heightPixels;
int screenHeight = getScreenHeight();//真实高度,包含NavigationBar
if (overlayCropPosition > 0 && cropPosition > overlayCropPosition) cropPosition = overlayCropPosition;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "height = " + height + " screenHeight = " + screenHeight + " cropPosition = " + cropPosition
+" b1.getWidth():"+b1.getWidth()+" b1.getHeight():"+b1.getHeight());
Bitmap b = Bitmap.createBitmap(b1, 0, 0, width, cropPosition > b1.getHeight() ? b1.getHeight() : cropPosition);//截图不包含导航栏
secondBitmapHeight = screenHeight - cropPosition;//secondBitmapHeight 就是导航栏高度
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "captureScreen end secondBitmapHeight = " + secondBitmapHeight+" b1.getHeight:"+ b1.getHeight()+" b.getHeight:"+ b.getHeight());
view.destroyDrawingCache();
return b;
}
在截到b1 = view.getDrawingCache();之后有对b1做裁剪。
cropPosition 是屏幕不包括导航栏的盖度,即1520-96=1424
Bitmap b = Bitmap.createBitmap(b1, 0, 0, width, cropPosition > b1.getHeight() ? b1.getHeight() : cropPosition);//截图不包含导航栏
裁剪过之后就是这样的,没有导航栏。
captureScreen方法带着一个没有导航栏的bitmap返回到startScroll()中。
private void startScroll() {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "startScroll");
scrollableView.setVerticalScrollBarEnabled(false);
Bitmap first = captureScreen(lContext, getWindow().getDecorView());//长截图会调到这里 有一次截图的操作
if(first == null) {
sendMessageToService(LONG_SCREENSHOT_UNKOWN_ERROR);
return;
}
bitmaps.add(first);
totalHeight = first.getHeight();
scrollController.startScroll();//第一次截好图后就开始scroll
}
先将其add到bitmaps中,然后调用scroller的startScroll()方法。这里就到了我们前面说的ScrollerController了。
protected void startScroll() {//(前面有过截图的操作)点击开始长截图后调用到这里,在SystemUI里面已经截图了一次
//view.setVerticalScrollBarEnabled(false);
setScrollDistanceListener();
maxScrollDistance = countMaxScrollDistance();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "startScroll:maxScrollDistance = " + maxScrollDistance);
continueScroll();
}
setScrollDistanceListener,就是在每次滚动结束之后会回调的runnable
protected void setScrollDistanceListener() {
view.setScrollDistanceRunnable(checkScrollDistance);
}
protected class CheckScrollDistanceRunnable implements Runnable {
public CheckScrollDistanceRunnable() {
}
@Override
public void run() {
int scrollDistance = computeScrollDistance();
if (lastItemView != null) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "run: lastItemPreBottom = " + lastItemPreBottom+" lastItemView.getBottom():" +lastItemView.getBottom());
scrollDistance = lastItemPreBottom - lastItemView.getBottom();
}
if ((scrollDistance != onceScrollDistance && scrollType != RECYCLEVIEW) || scrollType == SELF_WEBVIEW){
scrollDistance = fitScrollDistance(scrollDistance);
}
//if (scrollType != RECYCLEVIEW) scrollDistance = fitScrollDistance(scrollDistance);
if (scrollDistance > onceScrollDistance) scrollDistance = onceScrollDistance;
//if (scrollType == SELF_WEBVIEW && scrollDistance > leftScrollDistance) scrollDistance = leftScrollDistance;
//if (scrollType == SCROLLVIEW) scrollDistance = fitScrollDistance(scrollDistance);
Log.d(TAG, "scrollDistance = " + scrollDistance);
sendScrollMessage(scrollDistance);
}
}
continueScroll,首先会判断当前view是否能向下滚动,canScrollDown
protected void continueScroll() {
//view.removeCallbacks(scrollTimeoutRunnable);
if (this.canScrollDown()) {
scrolltimes += 1;
startScrollY = computeCurrentScrollY();
lastItemView = view.getChildAt(view.getChildCount() - 1);
if (lastItemView != null) lastItemPreBottom = lastItemView.getBottom();
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "ScrollController continueScroll: lastItemPreBottom = " + lastItemPreBottom+" scrollBy==>onceScrollDistance:"+onceScrollDistance
+" view.getChildCount():"+view.getChildCount());
view.scrollBy(0, onceScrollDistance);
//view.scrollTo(0, view.getScrollY() + onceScrollDistance);
//view.postDelayed(scrollTimeoutRunnable,300);
} else {
setScrollComplete();
}
}
RecyclerViewController的canScrollDown(),最大可滚动的距离 - 当前已经滚动的距离 = 剩余可滚动的距离,如果当前可滚动的距离 >= 最大允许滚动的距离 就不能在向下滚动了。否则就滚动view.scrollBy(0, onceScrollDistance);,onceScrollDistance的距离,该值在前面设置了,是屏幕高度的 3/4。
@Override
protected boolean canScrollDown() {
boolean result = true;
try {
int verticalScrollRange = (Integer)m1.invoke(view);
int verticalScrollExtent = (Integer)m2.invoke(view);
int maxDistance = verticalScrollRange - verticalScrollExtent;
int currentScrollDistance = (Integer)m3.invoke(view);
leftMaxScrollDistance = maxDistance - currentScrollDistance;
Log.d(TAG, "maxDistance = " + maxDistance + ", leftMaxScrollDistance = " + leftMaxScrollDistance + " scrollY = " + view.getScrollY());
if (currentScrollDistance >= maxDistance || preLeftMaxScrollDistance == leftMaxScrollDistance) {
result = false;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "RecyclerView has reached bottom");
}
} catch (Exception e) {
e.printStackTrace();
}
Log.d(TAG, "canScrollDown:result = " + result);
return result;
}
滚动结束后,回到前面设置的runnable
protected class CheckScrollDistanceRunnable implements Runnable {
public CheckScrollDistanceRunnable() {
}
@Override
public void run() {
int scrollDistance = computeScrollDistance();
if (lastItemView != null) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "run: lastItemPreBottom = " + lastItemPreBottom+" lastItemView.getBottom():" +lastItemView.getBottom());
scrollDistance = lastItemPreBottom - lastItemView.getBottom();
}
if ((scrollDistance != onceScrollDistance && scrollType != RECYCLEVIEW) || scrollType == SELF_WEBVIEW){
scrollDistance = fitScrollDistance(scrollDistance);
}
//if (scrollType != RECYCLEVIEW) scrollDistance = fitScrollDistance(scrollDistance);
if (scrollDistance > onceScrollDistance) scrollDistance = onceScrollDistance;
//if (scrollType == SELF_WEBVIEW && scrollDistance > leftScrollDistance) scrollDistance = leftScrollDistance;
//if (scrollType == SCROLLVIEW) scrollDistance = fitScrollDistance(scrollDistance);
Log.d(TAG, "scrollDistance = " + scrollDistance);
sendScrollMessage(scrollDistance);
}
}
计算出scrollDistance,传给最后的关键方法sendScrollMessage
protected void sendScrollMessage(int scrollDistance) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "sendScrollMessage: scrollDistance=" + scrollDistance, new Throwable());
if (scrollDistance > 0) {
Message shotMessage = scrollHandler.obtainMessage(CAPTURE_SCREEN, scrollDistance);
scrollHandler.sendMessage(shotMessage);
}
if (scrollDistance < onceScrollDistance) {
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "should have scroll to bottom");
Message completeMessage = scrollHandler.obtainMessage(SCROLL_COMPLETE);
scrollHandler.sendMessage(completeMessage);
}
}
滚动距离 > 0,发送一个CAPTURE_SCREEN的消息,滚动距离小于 设定的屏幕高度*3/4,证明滚动到底了,发送SCROLL_COMPLETE消息。我们一条条来看。
private Handler scrollHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case CAPTURE_SCREEN:
Bitmap temp = captureScreen(lContext, getWindow().getDecorView());
if(temp == null) {
sendMessageToService(LONG_SCREENSHOT_UNKOWN_ERROR);
return;
}
int scrollDistance = (Integer) msg.obj;
Bitmap b = Bitmap.createBitmap(temp, 0, temp.getHeight() - scrollDistance, temp.getWidth(), scrollDistance);//裁剪出滚出来的距离内容。
shotOnce(b);
break;
case SCROLL_COMPLETE:
if(!isScreenshotError) {
scrollComplete("scroll to bottom!");
}
break;
}
}
};
CAPTURE_SCREEN:第一步截图,这个方法我们前面分析过,截出来的是不包括导航高度,包括状态栏高度但是状态栏上只有颜色,没有内容的图片。
我们想象下,如果最后是拼接的这样的图片,肯定是有重复的,比如Settings顶部的搜索框肯定就一直再重复。
事实上长截图出来的肯定是不重复的。在得到截图后有个关键操作
Bitmap b = Bitmap.createBitmap(temp, 0, temp.getHeight() - scrollDistance, temp.getWidth(), scrollDistance);//裁剪出滚出来的距离内容。
这里对截到的图裁剪了, temp.getHeight() - scrollDistance ,图片高度 - 滚动距离 = 滚动出来的内容顶端。
很简单呀,滚动了 100px,就有100px的新内容出来,用总高度 - 100px 就是这新滚出来的内容的top值,然后向下裁剪scrollDistance的距离。所以裁剪过之后的bitmap就是滚动出来的那100px的内容。
真的很巧妙吧!!!
接下来调用shotOnce
private void shotOnce(Bitmap b) {
totalHeight += b.getHeight();
//currentSavingBitmap = b;
if (totalHeight + secondBitmapHeight >= LONG_BITMAP_HEIGHT_LIMIT || !scrollController.canScrollDown()) {
//sendMessageToService(UPDATE_LONG_SCREENSHOT);
if (!scrollController.canScrollDown() && totalHeight + secondBitmapHeight < LONG_BITMAP_HEIGHT_LIMIT) bitmaps.add(b);
scrollComplete("totalHeight + secondBitmapHeight >= LONG_BITMAP_HEIGHT_LIMIT || !scrollController.canScrollDown()");
} else {
bitmaps.add(b);
continueScroll();
}
}
是对长截图的最终高度做了一个限制,假设我们此次还没有达到限制。走到到else里面去了,就是把刚刚截出来的bitmap add到集合中,然后continueScroll()。
private void continueScroll() {
//if (!scrollController.canScrollDown()) return;
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "continueScroll");
if (scrollController != null) scrollController.continueScroll();
}
continueScroll就是在调用对应scrollController的continueScroll(),回到前面的流程,这次我们假设滚动到底了,即滚动距离 < onceScrollDistance ,这次就会发送两条消息。CAPTURE_SCREEN、SCROLL_COMPLETE。截图的消息就不说了,来看SCROLL_COMPLETE的消息。
case SCROLL_COMPLETE:
if(!isScreenshotError) {
scrollComplete("scroll to bottom!");
}
break;
private void scrollComplete(String reason) {
if (scrollCompleted) {
Log.d(TAG, "scrollCompleted, return");
return;
}
if (DEBUG_LONG_SCREENSHOT) Log.d(TAG, "scrollComplete: reason = " + reason);
if (overlayViews.size() > 0) {
for (int i=0; i<overlayViews.size(); i++) {
View view = overlayViews.get(i);
view.setVisibility(View.VISIBLE);
}
Bitmap temp = captureScreen(lContext, getWindow().getDecorView());
Bitmap overlayBitmap = Bitmap.createBitmap(temp, 0, getOverlayViewTop(), temp.getWidth(), temp.getHeight() - getOverlayViewTop());
bitmaps.add(overlayBitmap);
}
Message msg = Message.obtain(null, UPDATE_LONG_SCREENSHOT,
getOverlayViewTop(), secondBitmapHeight);
msg.obj = mergeBitmaps(bitmaps);
sendMessageToService(msg);
scrollCompleted = true;
}
msg.obj = mergeBitmaps(bitmaps) 将我们前面截图的图片合成一个bitmap
private Bitmap mergeBitmaps(List<Bitmap> bitmaps) {
if (bitmaps.size() == 0) return null;
if (bitmaps.size() == 1) return bitmaps.get(0);
int height = 0;
for (int i=0; i<bitmaps.size(); i++) {
Bitmap b = bitmaps.get(i);
height += b.getHeight();
Log.e(TAG,"mergeBitmaps i"+i+":"+b.getHeight());
}
Log.d(TAG, "mergeBitmaps:totalheight = " + height);
Bitmap result = Bitmap.createBitmap(bitmaps.get(0).getWidth(), height, Bitmap.Config.ARGB_8888);
int currentHight = 0;
Canvas canvas = new Canvas(result);
canvas.setHwBitmapsInSwModeEnabled(true);
for (int i=0; i<bitmaps.size(); i++) {
Bitmap b = bitmaps.get(i);
canvas.drawBitmap(b, 0, currentHight, null);
canvas.save(Canvas.ALL_SAVE_FLAG);
canvas.restore();
currentHight += b.getHeight();
}
return result;
}
然后给SystemUI发送UPDATE_LONG_SCREENSHOT消息,并且将组合好的图片传过去了。这里可以想像这个图片就是长截图,但是没有导航栏,有状态栏但是没有内容,是个半成品。
回到SystemUI的TakeScreenshotService
case UPDATE_LONG_SCREENSHOT:
Bitmap b = (Bitmap)msg.obj;
Log.d(TAG, "Bitmap height = " + b.getHeight());
int overlayViewsTop = msg.arg1;
int secondBitmapHeight = msg.arg2;
mScreenshot.updateLongScreenshotView(b, overlayViewsTop, secondBitmapHeight);
break;
mScreenshotView.setImageBitmap(longBitmap); 将longBitmap设置给mScreenshotView。
长截图滚动的动画
来关注滚动结束之后的操作。
主要是断开与TakeScreenshotService的连接,然后重置一些变量
public void stopLongscreenshot() {
if (!isInLongscreenshot) {
screenshotService.sendLongscreenshotMessage(TakeScreenshotService.CANCEL_LONG_SCREENSHOT);
setIsInLongshot(0);
return;
}
screenshotService.sendLongscreenshotMessage(TakeScreenshotService.STOP_LONG_SCREENSHOT);
try {
mContext.unregisterReceiver(mStopLongScreenshotReceiver);
} catch (Exception e) {
e.printStackTrace();
}
if (longBitmap == null) longBitmap = mScreenBitmap;
setIsInLongshot(0);
isInLongscreenshot = false;
//add buy cyy start for SPCSS00634821
if (mSecondScreenshotView != null) {
mSecondScreenshotView.setVisibility(View.GONE);
}
//add buy cyy start for SPCSS00634821
}
关键在最后 saveAndShowLongScreenShot
至此整个流程基本完成。