Files
lawnchair/src/com/android/launcher3/views/FloatingIconView.java
Sebastian Franco 7cdc616a01 Changing global variable for local variable to prevent memory leaks
The array mTmpObjArray is not properly clean but ther is no need
for the array to be global since every time is used is provided
so a use of a local variable will make sure is never leaked.

Fix: 269163620
Test: the flake related to memory leak on testWorkspace should dissapear
Change-Id: I29fb3dd6419f621f2ccce9db2fc5ed86358f7516
2023-03-27 11:18:14 -07:00

708 lines
27 KiB
Java

/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.views;
import static android.view.Gravity.LEFT;
import static com.android.launcher3.Utilities.getBadge;
import static com.android.launcher3.Utilities.getFullDrawable;
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.CancellationSignal;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.shortcuts.DeepShortcutView;
import java.util.function.Supplier;
/**
* A view that is created to look like another view with the purpose of creating fluid animations.
*/
@TargetApi(Build.VERSION_CODES.Q)
public class FloatingIconView extends FrameLayout implements
Animator.AnimatorListener, OnGlobalLayoutListener, FloatingView {
private static final String TAG = FloatingIconView.class.getSimpleName();
// Manages loading the icon on a worker thread
private static @Nullable IconLoadResult sIconLoadResult;
private static long sFetchIconId = 0;
private static long sRecycledFetchIconId = sFetchIconId;
public static final float SHAPE_PROGRESS_DURATION = 0.10f;
private static final RectF sTmpRectF = new RectF();
private Runnable mEndRunnable;
private CancellationSignal mLoadIconSignal;
private final Launcher mLauncher;
private final boolean mIsRtl;
private boolean mIsOpening;
private IconLoadResult mIconLoadResult;
private View mBtvDrawable;
private ClipIconView mClipIconView;
private @Nullable Drawable mBadge;
// A view whose visibility should update in sync with mOriginalIcon.
private @Nullable View mMatchVisibilityView;
// A view that will fade out as the animation progresses.
private @Nullable View mFadeOutView;
private View mOriginalIcon;
private RectF mPositionOut;
private Runnable mOnTargetChangeRunnable;
private final Rect mFinalDrawableBounds = new Rect();
private ListenerView mListenerView;
private Runnable mFastFinishRunnable;
private float mIconOffsetY;
public FloatingIconView(Context context) {
this(context, null);
}
public FloatingIconView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mLauncher = Launcher.getLauncher(context);
mIsRtl = Utilities.isRtl(getResources());
mListenerView = new ListenerView(context, attrs);
mClipIconView = new ClipIconView(context, attrs);
mBtvDrawable = new ImageView(context, attrs);
addView(mBtvDrawable);
addView(mClipIconView);
setWillNotDraw(false);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mIsOpening) {
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
}
@Override
protected void onDetachedFromWindow() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
super.onDetachedFromWindow();
}
/**
* Positions this view to match the size and location of {@param rect}.
* @param alpha The alpha[0, 1] of the entire floating view.
* @param progress A value from [0, 1] that represents the animation progress.
* @param shapeProgressStart The progress value at which to start the shape reveal.
* @param cornerRadius The corner radius of {@param rect}.
* @param isOpening True if view is used for app open animation, false for app close animation.
*/
public void update(float alpha, RectF rect, float progress, float shapeProgressStart,
float cornerRadius, boolean isOpening) {
setAlpha(alpha);
mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, this,
mLauncher.getDeviceProfile());
if (mFadeOutView != null) {
// The alpha goes from 1 to 0 when progress is 0 and 0.33 respectively.
mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.33f, 0, 1, LINEAR)));
}
}
@Override
public void onAnimationEnd(Animator animator) {
if (mLoadIconSignal != null) {
mLoadIconSignal.cancel();
}
if (mEndRunnable != null) {
mEndRunnable.run();
} else {
// End runnable also ends the reveal animator, so we manually handle it here.
mClipIconView.endReveal();
}
}
/**
* Sets the size and position of this view to match {@param v}.
*
* @param v The view to copy
* @param positionOut Rect that will hold the size and position of v.
*/
private void matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut) {
getLocationBoundsForView(launcher, v, isOpening, positionOut);
final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams(
Math.round(positionOut.width()),
Math.round(positionOut.height()));
updatePosition(positionOut, lp);
setLayoutParams(lp);
// For code simplicity, we always layout the child views using Gravity.LEFT
// and manually handle RTL for FloatingIconView when positioning it on the screen.
mClipIconView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT));
mBtvDrawable.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT));
}
private void updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
mPositionOut.set(pos);
lp.ignoreInsets = true;
// Position the floating view exactly on top of the original
lp.topMargin = Math.round(pos.top);
if (mIsRtl) {
lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - pos.right));
} else {
lp.setMarginStart(Math.round(pos.left));
}
// Set the properties here already to make sure they are available when running the first
// animation frame.
int left = mIsRtl
? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
: lp.leftMargin;
layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
}
private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
RectF outRect) {
getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect());
}
/**
* Gets the location bounds of a view and returns the overall rotation.
* - For DeepShortcutView, we return the bounds of the icon view.
* - For BubbleTextView, we return the icon bounds.
*/
public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
RectF outRect, Rect outViewBounds) {
boolean ignoreTransform = !isOpening;
if (v instanceof BubbleTextHolder) {
v = ((BubbleTextHolder) v).getBubbleText();
ignoreTransform = false;
} else if (v.getParent() instanceof DeepShortcutView) {
v = ((DeepShortcutView) v.getParent()).getIconView();
ignoreTransform = false;
}
if (v == null) {
return;
}
if (v instanceof BubbleTextView) {
((BubbleTextView) v).getIconBounds(outViewBounds);
} else if (v instanceof FolderIcon) {
((FolderIcon) v).getPreviewBounds(outViewBounds);
} else {
outViewBounds.set(0, 0, v.getWidth(), v.getHeight());
}
Utilities.getBoundsForViewInDragLayer(launcher.getDragLayer(), v, outViewBounds,
ignoreTransform, null /** recycle */, outRect);
}
/**
* Loads the icon and saves the results to {@link #sIconLoadResult}.
* Runs onIconLoaded callback (if any), which signifies that the FloatingIconView is
* ready to display the icon. Otherwise, the FloatingIconView will grab the results when its
* initialized.
*
* @param originalView The View that the FloatingIconView will replace.
* @param info ItemInfo of the originalView
* @param pos The position of the view.
* @param btvIcon The drawable of the BubbleTextView. May be null if original view is not a BTV
* @param outIconLoadResult We store the icon results into this object.
*/
@WorkerThread
@SuppressWarnings("WrongThread")
private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos,
@Nullable Drawable btvIcon, IconLoadResult outIconLoadResult) {
Drawable drawable;
boolean supportsAdaptiveIcons = !info.isDisabled(); // Use original icon for disabled icons.
Drawable badge = null;
if (info instanceof SystemShortcut) {
if (originalView instanceof ImageView) {
drawable = ((ImageView) originalView).getDrawable();
} else if (originalView instanceof DeepShortcutView) {
drawable = ((DeepShortcutView) originalView).getIconView().getBackground();
} else {
drawable = originalView.getBackground();
}
} else if (btvIcon instanceof PreloadIconDrawable) {
// Force the progress bar to display.
drawable = btvIcon;
} else {
int width = (int) pos.width();
int height = (int) pos.height();
Object[] tmpObjArray = new Object[1];
if (supportsAdaptiveIcons) {
boolean shouldThemeIcon = btvIcon instanceof FastBitmapDrawable
&& ((FastBitmapDrawable) btvIcon).isThemed();
drawable = getFullDrawable(l, info, width, height, shouldThemeIcon, tmpObjArray);
if (drawable instanceof AdaptiveIconDrawable) {
badge = getBadge(l, info, tmpObjArray[0]);
} else {
// The drawable we get back is not an adaptive icon, so we need to use the
// BubbleTextView icon that is already legacy treated.
drawable = btvIcon;
}
} else {
if (originalView instanceof BubbleTextView) {
// Similar to DragView, we simply use the BubbleTextView icon here.
drawable = btvIcon;
} else {
drawable = getFullDrawable(l, info, width, height, true /* shouldThemeIcon */,
tmpObjArray);
}
}
}
drawable = drawable == null ? null : drawable.getConstantState().newDrawable();
int iconOffset = getOffsetForIconBounds(l, drawable, pos);
// Clone right away as we are on the background thread instead of blocking the
// main thread later
Drawable btvClone = btvIcon == null ? null : btvIcon.getConstantState().newDrawable();
synchronized (outIconLoadResult) {
outIconLoadResult.btvDrawable = () -> btvClone;
outIconLoadResult.drawable = drawable;
outIconLoadResult.badge = badge;
outIconLoadResult.iconOffset = iconOffset;
if (outIconLoadResult.onIconLoaded != null) {
l.getMainExecutor().execute(outIconLoadResult.onIconLoaded);
outIconLoadResult.onIconLoaded = null;
}
outIconLoadResult.isIconLoaded = true;
}
}
/**
* Sets the drawables of the {@param originalView} onto this view.
*
* @param drawable The drawable of the original view.
* @param badge The badge of the original view.
* @param iconOffset The amount of offset needed to match this view with the original view.
*/
@UiThread
private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge,
@Nullable Supplier<Drawable> btvIcon, int iconOffset) {
final DeviceProfile dp = mLauncher.getDeviceProfile();
final InsettableFrameLayout.LayoutParams lp =
(InsettableFrameLayout.LayoutParams) getLayoutParams();
mBadge = badge;
mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, dp);
if (drawable instanceof AdaptiveIconDrawable) {
final int originalHeight = lp.height;
final int originalWidth = lp.width;
mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
if (dp.isLandscape) {
lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
} else {
lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
}
setLayoutParams(lp);
final LayoutParams clipViewLp = (LayoutParams) mClipIconView.getLayoutParams();
if (mBadge != null) {
Rect badgeBounds = new Rect(0, 0, clipViewLp.width, clipViewLp.height);
FastBitmapDrawable.setBadgeBounds(mBadge, badgeBounds);
}
clipViewLp.width = lp.width;
clipViewLp.height = lp.height;
mClipIconView.setLayoutParams(clipViewLp);
}
setOriginalDrawableBackground(btvIcon);
invalidate();
}
/**
* Draws the drawable of the BubbleTextView behind ClipIconView
*
* This is used to:
* - Have icon displayed while Adaptive Icon is loading
* - Displays the built in shadow to ensure a clean handoff
*
* Allows nullable as this may be cleared when drawing is deferred to ClipIconView.
*/
private void setOriginalDrawableBackground(@Nullable Supplier<Drawable> btvIcon) {
if (!mIsOpening) {
mBtvDrawable.setBackground(btvIcon == null ? null : btvIcon.get());
}
}
/**
* Returns true if the icon is different from main app icon
*/
public boolean isDifferentFromAppIcon() {
return mIconLoadResult == null ? false : mIconLoadResult.isThemed;
}
/**
* Checks if the icon result is loaded. If true, we set the icon immediately. Else, we add a
* callback to set the icon once the icon result is loaded.
*/
private void checkIconResult() {
CancellationSignal cancellationSignal = new CancellationSignal();
if (mIconLoadResult == null) {
Log.w(TAG, "No icon load result found in checkIconResult");
return;
}
synchronized (mIconLoadResult) {
if (mIconLoadResult.isIconLoaded) {
setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset);
setVisibility(VISIBLE);
updateViewsVisibility(false /* isVisible */);
} else {
mIconLoadResult.onIconLoaded = () -> {
if (cancellationSignal.isCanceled()) {
return;
}
setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset);
setVisibility(VISIBLE);
updateViewsVisibility(false /* isVisible */);
};
mLoadIconSignal = cancellationSignal;
}
}
}
@WorkerThread
@SuppressWarnings("WrongThread")
private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
if (!(drawable instanceof AdaptiveIconDrawable)) {
return 0;
}
int blurSizeOutline =
l.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline,
(int) position.height() + blurSizeOutline);
bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2);
try (LauncherIcons li = LauncherIcons.obtain(l)) {
Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable, null,
null, null));
}
bounds.inset(
(int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
(int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
);
return bounds.left;
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mBadge != null) {
mBadge.draw(canvas);
}
}
/**
* Sets a runnable that is called after a call to {@link #fastFinish()}.
*/
public void setFastFinishRunnable(Runnable runnable) {
mFastFinishRunnable = runnable;
}
@Override
public void fastFinish() {
if (mFastFinishRunnable != null) {
mFastFinishRunnable.run();
mFastFinishRunnable = null;
}
if (mLoadIconSignal != null) {
mLoadIconSignal.cancel();
mLoadIconSignal = null;
}
if (mEndRunnable != null) {
mEndRunnable.run();
mEndRunnable = null;
}
}
@Override
public void onAnimationStart(Animator animator) {
if ((mIconLoadResult != null && mIconLoadResult.isIconLoaded)
|| (!mIsOpening && mBtvDrawable.getBackground() != null)) {
// No need to wait for icon load since we can display the BubbleTextView drawable.
setVisibility(View.VISIBLE);
}
if (!mIsOpening) {
// When closing an app, we want the item on the workspace to be invisible immediately
updateViewsVisibility(false /* isVisible */);
}
}
@Override
public void onAnimationCancel(Animator animator) {}
@Override
public void onAnimationRepeat(Animator animator) {}
@Override
public void setPositionOffsetY(float y) {
mIconOffsetY = y;
onGlobalLayout();
}
@Override
public void onGlobalLayout() {
if (mOriginalIcon != null && mOriginalIcon.isAttachedToWindow() && mPositionOut != null) {
getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening, sTmpRectF);
sTmpRectF.offset(0, mIconOffsetY);
if (!sTmpRectF.equals(mPositionOut)) {
updatePosition(sTmpRectF, (InsettableFrameLayout.LayoutParams) getLayoutParams());
if (mOnTargetChangeRunnable != null) {
mOnTargetChangeRunnable.run();
}
}
}
}
public void setOnTargetChangeListener(Runnable onTargetChangeListener) {
mOnTargetChangeRunnable = onTargetChangeListener;
}
/**
* Loads the icon drawable on a worker thread to reduce latency between swapping views.
*/
@UiThread
public static IconLoadResult fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening) {
RectF position = new RectF();
getLocationBoundsForView(l, v, isOpening, position);
final FastBitmapDrawable btvIcon;
final Supplier<Drawable> btvDrawableSupplier;
if (v instanceof BubbleTextView) {
BubbleTextView btv = (BubbleTextView) v;
if (info instanceof ItemInfoWithIcon
&& (((ItemInfoWithIcon) info).runtimeStatusFlags
& ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
btvIcon = btv.makePreloadIcon();
btvDrawableSupplier = () -> btvIcon;
} else {
btvIcon = btv.getIcon();
// Clone when needed
btvDrawableSupplier = () -> btvIcon.getConstantState().newDrawable();
}
} else {
btvIcon = null;
btvDrawableSupplier = null;
}
IconLoadResult result = new IconLoadResult(info, btvIcon != null && btvIcon.isThemed());
result.btvDrawable = btvDrawableSupplier;
final long fetchIconId = sFetchIconId++;
MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
if (fetchIconId < sRecycledFetchIconId) {
return;
}
getIconResult(l, v, info, position, btvIcon, result);
});
sIconLoadResult = result;
return result;
}
/**
* Creates a floating icon view for {@param originalView}.
* @param originalView The view to copy
* @param visibilitySyncView A view whose visibility should update in sync with originalView.
* @param fadeOutView A view that will fade out as the animation progresses.
* @param hideOriginal If true, it will hide {@param originalView} while this view is visible.
* Else, we will not draw anything in this view.
* @param positionOut Rect that will hold the size and position of v.
* @param isOpening True if this view replaces the icon for app open animation.
*/
public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView,
@Nullable View visibilitySyncView, @Nullable View fadeOutView, boolean hideOriginal,
RectF positionOut, boolean isOpening) {
final DragLayer dragLayer = launcher.getDragLayer();
ViewGroup parent = (ViewGroup) dragLayer.getParent();
FloatingIconView view = launcher.getViewCache().getView(R.layout.floating_icon_view,
launcher, parent);
view.recycle();
// Init properties before getting the drawable.
view.mIsOpening = isOpening;
view.mOriginalIcon = originalView;
view.mMatchVisibilityView = visibilitySyncView;
view.mFadeOutView = fadeOutView;
view.mPositionOut = positionOut;
// Get the drawable on the background thread
boolean shouldLoadIcon = originalView.getTag() instanceof ItemInfo && hideOriginal;
if (shouldLoadIcon) {
if (sIconLoadResult != null && sIconLoadResult.itemInfo == originalView.getTag()) {
view.mIconLoadResult = sIconLoadResult;
} else {
view.mIconLoadResult = fetchIcon(launcher, originalView,
(ItemInfo) originalView.getTag(), isOpening);
}
view.setOriginalDrawableBackground(view.mIconLoadResult.btvDrawable);
}
sIconLoadResult = null;
// Match the position of the original view.
view.matchPositionOf(launcher, originalView, isOpening, positionOut);
// We need to add it to the overlay, but keep it invisible until animation starts..
view.setVisibility(View.INVISIBLE);
parent.addView(view);
dragLayer.addView(view.mListenerView);
view.mListenerView.setListener(view::fastFinish);
view.mEndRunnable = () -> {
view.mEndRunnable = null;
if (view.mFadeOutView != null) {
view.mFadeOutView.setAlpha(1f);
}
if (hideOriginal) {
view.updateViewsVisibility(true /* isVisible */);
view.finish(dragLayer);
} else {
view.finish(dragLayer);
}
};
// Must be called after matchPositionOf so that we know what size to load.
// Must be called after the fastFinish listener and end runnable is created so that
// the icon is not left in a hidden state.
if (shouldLoadIcon) {
view.checkIconResult();
}
return view;
}
private void updateViewsVisibility(boolean isVisible) {
if (mOriginalIcon != null) {
setIconAndDotVisible(mOriginalIcon, isVisible);
}
if (mMatchVisibilityView != null) {
setIconAndDotVisible(mMatchVisibilityView, isVisible);
}
}
private void finish(DragLayer dragLayer) {
((ViewGroup) dragLayer.getParent()).removeView(this);
dragLayer.removeView(mListenerView);
recycle();
mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this);
}
private void recycle() {
setTranslationX(0);
setTranslationY(0);
setScaleX(1);
setScaleY(1);
setAlpha(1);
if (mLoadIconSignal != null) {
mLoadIconSignal.cancel();
}
mLoadIconSignal = null;
mEndRunnable = null;
mFinalDrawableBounds.setEmpty();
mIsOpening = false;
mPositionOut = null;
mListenerView.setListener(null);
mOriginalIcon = null;
mOnTargetChangeRunnable = null;
mBadge = null;
sRecycledFetchIconId = sFetchIconId;
mIconLoadResult = null;
mClipIconView.recycle();
mBtvDrawable.setBackground(null);
mFastFinishRunnable = null;
mIconOffsetY = 0;
mMatchVisibilityView = null;
mFadeOutView = null;
}
private static class IconLoadResult {
final ItemInfo itemInfo;
final boolean isThemed;
Supplier<Drawable> btvDrawable;
Drawable drawable;
Drawable badge;
int iconOffset;
Runnable onIconLoaded;
boolean isIconLoaded;
IconLoadResult(ItemInfo itemInfo, boolean isThemed) {
this.itemInfo = itemInfo;
this.isThemed = isThemed;
}
}
}