mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 23:38:38 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,21 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class AllCapsTextView extends TextView {
|
||||
|
||||
public AllCapsTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(CharSequence text, BufferType type) {
|
||||
super.setText(text.toString().toUpperCase(), type);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.AppConstants.Versions;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
|
||||
/**
|
||||
* AnchoredPopup is the base class for doorhanger notifications, and is anchored to the urlbar.
|
||||
*/
|
||||
public abstract class AnchoredPopup extends PopupWindow {
|
||||
public interface OnVisibilityChangeListener {
|
||||
public void onDoorHangerShow();
|
||||
public void onDoorHangerHide();
|
||||
}
|
||||
|
||||
private View mAnchor;
|
||||
private OnVisibilityChangeListener onVisibilityChangeListener;
|
||||
|
||||
protected RoundedCornerLayout mContent;
|
||||
protected boolean mInflated;
|
||||
|
||||
protected final Context mContext;
|
||||
|
||||
public AnchoredPopup(Context context) {
|
||||
super(context);
|
||||
|
||||
mContext = context;
|
||||
|
||||
setAnimationStyle(R.style.PopupAnimation);
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
// Hide the default window background. Passing null prevents the below setOutTouchable()
|
||||
// call from working, so use an empty BitmapDrawable instead.
|
||||
setBackgroundDrawable(new BitmapDrawable(mContext.getResources()));
|
||||
|
||||
// Allow the popup to be dismissed when touching outside.
|
||||
setOutsideTouchable(true);
|
||||
|
||||
// PopupWindow has a default width and height of 0, so set the width here.
|
||||
int width = (int) mContext.getResources().getDimension(R.dimen.doorhanger_width);
|
||||
setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
setWidth(width);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
final View layout = inflater.inflate(R.layout.anchored_popup, null);
|
||||
setContentView(layout);
|
||||
|
||||
mContent = (RoundedCornerLayout) layout.findViewById(R.id.content);
|
||||
|
||||
mInflated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the anchor for this popup.
|
||||
*
|
||||
* @param anchor Anchor view for positioning the arrow.
|
||||
*/
|
||||
public void setAnchor(View anchor) {
|
||||
mAnchor = anchor;
|
||||
}
|
||||
|
||||
public void setOnVisibilityChangeListener(OnVisibilityChangeListener listener) {
|
||||
onVisibilityChangeListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the popup with the arrow pointing to the center of the anchor view. If the anchor
|
||||
* isn't visible, the popup will just be shown at the top of the root view.
|
||||
*/
|
||||
public void show() {
|
||||
if (!mInflated) {
|
||||
throw new IllegalStateException("ArrowPopup#init() must be called before ArrowPopup#show()");
|
||||
}
|
||||
|
||||
if (onVisibilityChangeListener != null) {
|
||||
onVisibilityChangeListener.onDoorHangerShow();
|
||||
}
|
||||
|
||||
final int[] anchorLocation = new int[2];
|
||||
if (mAnchor != null) {
|
||||
mAnchor.getLocationInWindow(anchorLocation);
|
||||
}
|
||||
|
||||
// The doorhanger should overlap the bottom of the urlbar.
|
||||
int offsetY = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetY);
|
||||
final View decorView = ((Activity) mContext).getWindow().getDecorView();
|
||||
|
||||
final boolean validAnchor = (mAnchor != null) && (anchorLocation[1] > 0);
|
||||
if (HardwareUtils.isTablet()) {
|
||||
if (validAnchor) {
|
||||
showAsDropDown(mAnchor, 0, 0);
|
||||
} else {
|
||||
// The anchor will be offscreen if the dynamic toolbar is hidden, so anticipate the re-shown position
|
||||
// of the toolbar.
|
||||
final int offsetX = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetX);
|
||||
showAtLocation(decorView, Gravity.TOP | Gravity.LEFT, offsetX, offsetY);
|
||||
}
|
||||
} else {
|
||||
// If the anchor is null or out of the window bounds, just show the popup at the top of the
|
||||
// root view.
|
||||
final View anchor = validAnchor ? mAnchor : decorView;
|
||||
|
||||
showAtLocation(anchor, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
if (onVisibilityChangeListener != null) {
|
||||
onVisibilityChangeListener.onDoorHangerHide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.animation.HeightChangeAnimation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
public class AnimatedHeightLayout extends RelativeLayout {
|
||||
private static final String LOGTAG = "GeckoAnimatedHeightLayout";
|
||||
private static final int ANIMATION_DURATION = 100;
|
||||
private boolean mAnimating;
|
||||
|
||||
public AnimatedHeightLayout(Context context) {
|
||||
super(context, null);
|
||||
}
|
||||
|
||||
public AnimatedHeightLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AnimatedHeightLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int oldHeight = getMeasuredHeight();
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
int newHeight = getMeasuredHeight();
|
||||
|
||||
if (!mAnimating && oldHeight != 0 && oldHeight != newHeight) {
|
||||
mAnimating = true;
|
||||
setMeasuredDimension(getMeasuredWidth(), oldHeight);
|
||||
|
||||
// Animate the difference of suggestion row height
|
||||
Animation anim = new HeightChangeAnimation(this, oldHeight, newHeight);
|
||||
anim.setDuration(ANIMATION_DURATION);
|
||||
anim.setInterpolator(new DecelerateInterpolator());
|
||||
anim.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {}
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finishAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
startAnimation(anim);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
finishAnimation();
|
||||
}
|
||||
|
||||
void finishAnimation() {
|
||||
if (mAnimating) {
|
||||
getLayoutParams().height = LayoutParams.WRAP_CONTENT;
|
||||
mAnimating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.ListView;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
|
||||
public class BasicColorPicker extends ListView {
|
||||
private final static String LOGTAG = "GeckoBasicColorPicker";
|
||||
private final static List<Integer> DEFAULT_COLORS = Arrays.asList(Color.rgb(215, 57, 32),
|
||||
Color.rgb(255, 134, 5),
|
||||
Color.rgb(255, 203, 19),
|
||||
Color.rgb(95, 173, 71),
|
||||
Color.rgb(84, 201, 168),
|
||||
Color.rgb(33, 161, 222),
|
||||
Color.rgb(16, 36, 87),
|
||||
Color.rgb(91, 32, 103),
|
||||
Color.rgb(212, 221, 228),
|
||||
Color.BLACK);
|
||||
|
||||
private static Drawable mCheckDrawable;
|
||||
int mSelected;
|
||||
final ColorPickerListAdapter mAdapter;
|
||||
|
||||
public BasicColorPicker(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public BasicColorPicker(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public BasicColorPicker(Context context, AttributeSet attrs, int style) {
|
||||
this(context, attrs, style, DEFAULT_COLORS);
|
||||
}
|
||||
|
||||
public BasicColorPicker(Context context, AttributeSet attrs, int style, List<Integer> colors) {
|
||||
super(context, attrs, style);
|
||||
mAdapter = new ColorPickerListAdapter(context, new ArrayList<Integer>(colors));
|
||||
setAdapter(mAdapter);
|
||||
|
||||
setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
mSelected = position;
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return mAdapter.getItem(mSelected);
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
if (!DEFAULT_COLORS.contains(color)) {
|
||||
mSelected = mAdapter.getCount();
|
||||
mAdapter.add(color);
|
||||
} else {
|
||||
mSelected = DEFAULT_COLORS.indexOf(color);
|
||||
}
|
||||
|
||||
setSelection(mSelected);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
Drawable getCheckDrawable() {
|
||||
if (mCheckDrawable == null) {
|
||||
Resources res = getContext().getResources();
|
||||
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, typedValue, true);
|
||||
DisplayMetrics metrics = new android.util.DisplayMetrics();
|
||||
((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics);
|
||||
int height = (int) typedValue.getDimension(metrics);
|
||||
|
||||
Drawable background = res.getDrawable(R.drawable.color_picker_row_bg);
|
||||
Rect r = new Rect();
|
||||
background.getPadding(r);
|
||||
height -= r.top + r.bottom;
|
||||
|
||||
mCheckDrawable = res.getDrawable(R.drawable.color_picker_checkmark);
|
||||
mCheckDrawable.setBounds(0, 0, height, height);
|
||||
}
|
||||
|
||||
return mCheckDrawable;
|
||||
}
|
||||
|
||||
private class ColorPickerListAdapter extends ArrayAdapter<Integer> {
|
||||
private final List<Integer> mColors;
|
||||
|
||||
public ColorPickerListAdapter(Context context, List<Integer> colors) {
|
||||
super(context, R.layout.color_picker_row, colors);
|
||||
mColors = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View v = super.getView(position, convertView, parent);
|
||||
|
||||
Drawable d = v.getBackground();
|
||||
d.setColorFilter(getItem(position), PorterDuff.Mode.MULTIPLY);
|
||||
v.setBackgroundDrawable(d);
|
||||
|
||||
Drawable check = null;
|
||||
CheckedTextView checked = ((CheckedTextView) v);
|
||||
if (mSelected == position) {
|
||||
check = getCheckDrawable();
|
||||
}
|
||||
|
||||
checked.setCompoundDrawables(check, null, null, null);
|
||||
checked.setText("");
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Checkable;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
|
||||
public class CheckableLinearLayout extends LinearLayout implements Checkable {
|
||||
|
||||
private CheckBox mCheckBox;
|
||||
|
||||
public CheckableLinearLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return mCheckBox != null && mCheckBox.isChecked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean isChecked) {
|
||||
if (mCheckBox != null) {
|
||||
mCheckBox.setChecked(isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggle() {
|
||||
if (mCheckBox != null) {
|
||||
mCheckBox.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
mCheckBox = (CheckBox) findViewById(R.id.checkbox);
|
||||
mCheckBox.setClickable(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.EditText;
|
||||
|
||||
public class ClickableWhenDisabledEditText extends EditText {
|
||||
public ClickableWhenDisabledEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (!isEnabled() && event.getAction() == MotionEvent.ACTION_UP) {
|
||||
return performClick();
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.toolbar.SiteIdentityPopup;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class ContentSecurityDoorHanger extends DoorHanger {
|
||||
private static final String LOGTAG = "GeckoSecurityDoorHanger";
|
||||
|
||||
private final TextView mTitle;
|
||||
private final TextView mSecurityState;
|
||||
private final TextView mMessage;
|
||||
|
||||
public ContentSecurityDoorHanger(Context context, DoorhangerConfig config, Type type) {
|
||||
super(context, config, type);
|
||||
|
||||
mTitle = (TextView) findViewById(R.id.security_title);
|
||||
mSecurityState = (TextView) findViewById(R.id.security_state);
|
||||
mMessage = (TextView) findViewById(R.id.security_message);
|
||||
|
||||
loadConfig(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadConfig(DoorhangerConfig config) {
|
||||
final String message = config.getMessage();
|
||||
if (message != null) {
|
||||
mMessage.setText(message);
|
||||
}
|
||||
|
||||
final JSONObject options = config.getOptions();
|
||||
if (options != null) {
|
||||
setOptions(options);
|
||||
}
|
||||
|
||||
final DoorhangerConfig.Link link = config.getLink();
|
||||
if (link != null) {
|
||||
addLink(link.label, link.url);
|
||||
}
|
||||
|
||||
addButtonsToLayout(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getContentResource() {
|
||||
return R.layout.doorhanger_security;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOptions(final JSONObject options) {
|
||||
super.setOptions(options);
|
||||
final JSONObject link = options.optJSONObject("link");
|
||||
if (link != null) {
|
||||
try {
|
||||
final String linkLabel = link.getString("label");
|
||||
final String linkUrl = link.getString("url");
|
||||
addLink(linkLabel, linkUrl);
|
||||
} catch (JSONException e) { }
|
||||
}
|
||||
|
||||
final JSONObject trackingProtection = options.optJSONObject("tracking_protection");
|
||||
if (trackingProtection != null) {
|
||||
mTitle.setVisibility(VISIBLE);
|
||||
mTitle.setText(R.string.doorhanger_tracking_title);
|
||||
try {
|
||||
final boolean enabled = trackingProtection.getBoolean("enabled");
|
||||
if (enabled) {
|
||||
mMessage.setText(R.string.doorhanger_tracking_message_enabled);
|
||||
mSecurityState.setText(R.string.doorhanger_tracking_state_enabled);
|
||||
mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.affirmative_green));
|
||||
} else {
|
||||
mMessage.setText(R.string.doorhanger_tracking_message_disabled);
|
||||
mSecurityState.setText(R.string.doorhanger_tracking_state_disabled);
|
||||
mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.rejection_red));
|
||||
}
|
||||
mMessage.setVisibility(VISIBLE);
|
||||
mSecurityState.setVisibility(VISIBLE);
|
||||
} catch (JSONException e) { }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
|
||||
return new Button.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
|
||||
|
||||
final JSONObject response = new JSONObject();
|
||||
try {
|
||||
switch (mType) {
|
||||
case TRACKING:
|
||||
response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal()));
|
||||
response.put("contentType", ("tracking"));
|
||||
break;
|
||||
default:
|
||||
Log.w(LOGTAG, "Unknown doorhanger type " + mType.toString());
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error creating onClick response", e);
|
||||
}
|
||||
|
||||
mOnButtonClickListener.onButtonClick(response, ContentSecurityDoorHanger.this);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.mozilla.gecko.widget.themed.ThemedImageView;
|
||||
|
||||
/**
|
||||
* An ImageView which will always display at the given width and calculated height (based on the width and
|
||||
* the supplied aspect ratio), drawn starting from the top left hand corner. A supplied drawable will be resized to fit
|
||||
* the width of the view; if the resized drawable is too tall for the view then the drawable will be cropped at the
|
||||
* bottom, however if the resized drawable is too short for the view to display whilst honouring it's given width and
|
||||
* height then the drawable will be displayed at full height with the right hand side cropped.
|
||||
*/
|
||||
public abstract class CropImageView extends ThemedImageView {
|
||||
public static final String LOGTAG = "Gecko" + CropImageView.class.getSimpleName();
|
||||
|
||||
private int viewWidth;
|
||||
private int viewHeight;
|
||||
private int drawableWidth;
|
||||
private int drawableHeight;
|
||||
|
||||
private boolean resize = true;
|
||||
private Matrix layoutCurrentMatrix = new Matrix();
|
||||
private Matrix layoutNextMatrix = new Matrix();
|
||||
|
||||
|
||||
public CropImageView(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CropImageView(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CropImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
protected abstract float getAspectRatio();
|
||||
|
||||
protected void init() {
|
||||
// Setting the pivots means that the image will be drawn from the top left hand corner. There are
|
||||
// issues in Android 4.1 (16) which mean setting these values to 0 may not work.
|
||||
// http://stackoverflow.com/questions/26658124/setpivotx-doesnt-work-on-android-4-1-1-nineoldandroids
|
||||
setPivotX(1);
|
||||
setPivotY(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the view to determine the measured width and height.
|
||||
* The height is constrained by the measured width.
|
||||
*
|
||||
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
|
||||
* @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored.
|
||||
*/
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
// Default measuring.
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
// Force the height based on the aspect ratio.
|
||||
viewWidth = getMeasuredWidth();
|
||||
viewHeight = (int) (viewWidth * getAspectRatio());
|
||||
|
||||
setMeasuredDimension(viewWidth, viewHeight);
|
||||
|
||||
updateImageMatrix();
|
||||
}
|
||||
|
||||
protected void updateImageMatrix() {
|
||||
if (!resize || getDrawable() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScaleType(ImageView.ScaleType.MATRIX);
|
||||
|
||||
getDrawable().setBounds(0, 0, viewWidth, viewHeight);
|
||||
|
||||
final float horizontalScaleValue = (float) viewWidth / (float) drawableWidth;
|
||||
final float verticalScaleValue = (float) viewHeight / (float) drawableHeight;
|
||||
|
||||
final float scale = Math.max(verticalScaleValue, horizontalScaleValue);
|
||||
|
||||
layoutNextMatrix.reset();
|
||||
layoutNextMatrix.setScale(scale, scale);
|
||||
setImageMatrix(layoutNextMatrix);
|
||||
|
||||
// You can't modify the matrix in place and we want to avoid allocation, so let's keep two references to two
|
||||
// different matrix objects that we can swap when the values need to change
|
||||
final Matrix swapReferenceMatrix = layoutCurrentMatrix;
|
||||
layoutCurrentMatrix = layoutNextMatrix;
|
||||
layoutNextMatrix = swapReferenceMatrix;
|
||||
}
|
||||
|
||||
public void setImageBitmap(final Bitmap bm, final boolean resize) {
|
||||
super.setImageBitmap(bm);
|
||||
|
||||
this.resize = resize;
|
||||
updateImageMatrix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageResource(final int resId) {
|
||||
super.setImageResource(resId);
|
||||
setImageMatrix(null);
|
||||
resize = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageDrawable(final Drawable drawable) {
|
||||
this.setImageDrawable(drawable, false);
|
||||
}
|
||||
|
||||
public void setImageDrawable(final Drawable drawable, final boolean resize) {
|
||||
super.setImageDrawable(drawable);
|
||||
|
||||
if (drawable != null) {
|
||||
// Reset the matrix to ensure that any previous changes aren't carried through.
|
||||
setImageMatrix(null);
|
||||
|
||||
drawableWidth = drawable.getIntrinsicWidth();
|
||||
drawableHeight = drawable.getIntrinsicHeight();
|
||||
} else {
|
||||
drawableWidth = -1;
|
||||
drawableHeight = -1;
|
||||
}
|
||||
|
||||
this.resize = resize;
|
||||
|
||||
updateImageMatrix();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,665 @@
|
|||
/*
|
||||
* Copyright (C) 2007 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 org.mozilla.gecko.widget;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.AppConstants.Versions;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Display;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.CalendarView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.NumberPicker;
|
||||
|
||||
public class DateTimePicker extends FrameLayout {
|
||||
private static final boolean DEBUG = true;
|
||||
private static final String LOGTAG = "GeckoDateTimePicker";
|
||||
private static final int DEFAULT_START_YEAR = 1;
|
||||
private static final int DEFAULT_END_YEAR = 9999;
|
||||
private static final char DATE_FORMAT_DAY = 'd';
|
||||
private static final char DATE_FORMAT_MONTH = 'M';
|
||||
private static final char DATE_FORMAT_YEAR = 'y';
|
||||
|
||||
boolean mYearEnabled = true;
|
||||
boolean mMonthEnabled = true;
|
||||
boolean mWeekEnabled;
|
||||
boolean mDayEnabled = true;
|
||||
boolean mHourEnabled = true;
|
||||
boolean mMinuteEnabled = true;
|
||||
boolean mIs12HourMode;
|
||||
private boolean mCalendarEnabled;
|
||||
|
||||
// Size of the screen in inches;
|
||||
private final int mScreenWidth;
|
||||
private final int mScreenHeight;
|
||||
private final OnValueChangeListener mOnChangeListener;
|
||||
private final LinearLayout mPickers;
|
||||
private final LinearLayout mDateSpinners;
|
||||
private final LinearLayout mTimeSpinners;
|
||||
|
||||
final NumberPicker mDaySpinner;
|
||||
final NumberPicker mMonthSpinner;
|
||||
final NumberPicker mWeekSpinner;
|
||||
final NumberPicker mYearSpinner;
|
||||
final NumberPicker mHourSpinner;
|
||||
final NumberPicker mMinuteSpinner;
|
||||
final NumberPicker mAMPMSpinner;
|
||||
private final CalendarView mCalendar;
|
||||
private final EditText mDaySpinnerInput;
|
||||
private final EditText mMonthSpinnerInput;
|
||||
private final EditText mWeekSpinnerInput;
|
||||
private final EditText mYearSpinnerInput;
|
||||
private final EditText mHourSpinnerInput;
|
||||
private final EditText mMinuteSpinnerInput;
|
||||
private final EditText mAMPMSpinnerInput;
|
||||
private Locale mCurrentLocale;
|
||||
private String[] mShortMonths;
|
||||
private String[] mShortAMPMs;
|
||||
private int mNumberOfMonths;
|
||||
|
||||
Calendar mTempDate;
|
||||
Calendar mCurrentDate;
|
||||
private Calendar mMinDate;
|
||||
private Calendar mMaxDate;
|
||||
private final PickersState mState;
|
||||
|
||||
public static enum PickersState { DATE, MONTH, WEEK, TIME, DATETIME };
|
||||
|
||||
public class OnValueChangeListener implements NumberPicker.OnValueChangeListener {
|
||||
@Override
|
||||
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
|
||||
updateInputState();
|
||||
mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "SDK version > 10, using new behavior");
|
||||
}
|
||||
|
||||
// The native date picker widget on these SDKs increments
|
||||
// the next field when one field reaches the maximum.
|
||||
if (picker == mDaySpinner && mDayEnabled) {
|
||||
int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
|
||||
int old = mTempDate.get(Calendar.DAY_OF_MONTH);
|
||||
setTempDate(Calendar.DAY_OF_MONTH, old, newVal, 1, maxDayOfMonth);
|
||||
} else if (picker == mMonthSpinner && mMonthEnabled) {
|
||||
int old = mTempDate.get(Calendar.MONTH);
|
||||
setTempDate(Calendar.MONTH, old, newVal, Calendar.JANUARY, Calendar.DECEMBER);
|
||||
} else if (picker == mWeekSpinner) {
|
||||
int old = mTempDate.get(Calendar.WEEK_OF_YEAR);
|
||||
int maxWeekOfYear = mTempDate.getActualMaximum(Calendar.WEEK_OF_YEAR);
|
||||
setTempDate(Calendar.WEEK_OF_YEAR, old, newVal, 0, maxWeekOfYear);
|
||||
} else if (picker == mYearSpinner && mYearEnabled) {
|
||||
int month = mTempDate.get(Calendar.MONTH);
|
||||
mTempDate.set(Calendar.YEAR, newVal);
|
||||
// Changing the year shouldn't change the month. (in case of non-leap year a Feb 29)
|
||||
// change the day instead;
|
||||
if (month != mTempDate.get(Calendar.MONTH)) {
|
||||
mTempDate.set(Calendar.MONTH, month);
|
||||
mTempDate.set(Calendar.DAY_OF_MONTH,
|
||||
mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
} else if (picker == mHourSpinner && mHourEnabled) {
|
||||
if (mIs12HourMode) {
|
||||
setTempDate(Calendar.HOUR, oldVal, newVal, 1, 12);
|
||||
} else {
|
||||
setTempDate(Calendar.HOUR_OF_DAY, oldVal, newVal, 0, 23);
|
||||
}
|
||||
} else if (picker == mMinuteSpinner && mMinuteEnabled) {
|
||||
setTempDate(Calendar.MINUTE, oldVal, newVal, 0, 59);
|
||||
} else if (picker == mAMPMSpinner && mHourEnabled) {
|
||||
mTempDate.set(Calendar.AM_PM, newVal);
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
setDate(mTempDate);
|
||||
if (mDayEnabled) {
|
||||
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
if (mWeekEnabled) {
|
||||
mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR));
|
||||
}
|
||||
updateCalendar();
|
||||
updateSpinners();
|
||||
notifyDateChanged();
|
||||
}
|
||||
|
||||
private void setTempDate(int field, int oldVal, int newVal, int min, int max) {
|
||||
if (oldVal == max && newVal == min) {
|
||||
mTempDate.add(field, 1);
|
||||
} else if (oldVal == min && newVal == max) {
|
||||
mTempDate.add(field, -1);
|
||||
} else {
|
||||
mTempDate.add(field, newVal - oldVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
|
||||
final StringBuilder mBuilder = new StringBuilder();
|
||||
|
||||
final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US);
|
||||
|
||||
final Object[] mArgs = new Object[1];
|
||||
|
||||
@Override
|
||||
public String format(int value) {
|
||||
mArgs[0] = value;
|
||||
mBuilder.delete(0, mBuilder.length());
|
||||
mFmt.format("%02d", mArgs);
|
||||
return mFmt.toString();
|
||||
}
|
||||
};
|
||||
|
||||
private void displayPickers() {
|
||||
setWeekShown(false);
|
||||
set12HourShown(mIs12HourMode);
|
||||
if (mState == PickersState.DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHourShown(false);
|
||||
setMinuteShown(false);
|
||||
if (mState == PickersState.WEEK) {
|
||||
setDayShown(false);
|
||||
setMonthShown(false);
|
||||
setWeekShown(true);
|
||||
} else if (mState == PickersState.MONTH) {
|
||||
setDayShown(false);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimePicker(Context context) {
|
||||
this(context, "", "", PickersState.DATE, null, null);
|
||||
}
|
||||
|
||||
public DateTimePicker(Context context, String dateFormat, String dateTimeValue, PickersState state, String minDateValue, String maxDateValue) {
|
||||
super(context);
|
||||
|
||||
setCurrentLocale(Locale.getDefault());
|
||||
|
||||
mState = state;
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
inflater.inflate(R.layout.datetime_picker, this, true);
|
||||
|
||||
mOnChangeListener = new OnValueChangeListener();
|
||||
|
||||
mDateSpinners = (LinearLayout)findViewById(R.id.date_spinners);
|
||||
mTimeSpinners = (LinearLayout)findViewById(R.id.time_spinners);
|
||||
mPickers = (LinearLayout)findViewById(R.id.datetime_picker);
|
||||
|
||||
// We will display differently according to the screen size width.
|
||||
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
Display display = wm.getDefaultDisplay();
|
||||
DisplayMetrics dm = new DisplayMetrics();
|
||||
display.getMetrics(dm);
|
||||
mScreenWidth = display.getWidth() / dm.densityDpi;
|
||||
mScreenHeight = display.getHeight() / dm.densityDpi;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "screen width: " + mScreenWidth + " screen height: " + mScreenHeight);
|
||||
}
|
||||
|
||||
// Set the min / max attribute.
|
||||
try {
|
||||
if (minDateValue != null && !minDateValue.equals("")) {
|
||||
mMinDate.setTime(new SimpleDateFormat(dateFormat).parse(minDateValue));
|
||||
} else {
|
||||
mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.e(LOGTAG, "Error parsing format sting: " + ex);
|
||||
mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
|
||||
}
|
||||
|
||||
try {
|
||||
if (maxDateValue != null && !maxDateValue.equals("")) {
|
||||
mMaxDate.setTime(new SimpleDateFormat(dateFormat).parse(maxDateValue));
|
||||
} else {
|
||||
mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.e(LOGTAG, "Error parsing format string: " + ex);
|
||||
mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
|
||||
}
|
||||
|
||||
// Find the initial date from the constructor arguments.
|
||||
try {
|
||||
if (!dateTimeValue.equals("")) {
|
||||
mTempDate.setTime(new SimpleDateFormat(dateFormat).parse(dateTimeValue));
|
||||
} else {
|
||||
mTempDate.setTimeInMillis(System.currentTimeMillis());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.e(LOGTAG, "Error parsing format string: " + ex);
|
||||
mTempDate.setTimeInMillis(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
if (mMaxDate.before(mMinDate)) {
|
||||
// If the input date range is illogical/garbage, we should not restrict the input range (i.e. allow the
|
||||
// user to select any date). If we try to make any assumptions based on the illogical min/max date we could
|
||||
// potentially prevent the user from selecting dates that are in the developers intended range, so it's best
|
||||
// to allow anything.
|
||||
mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
|
||||
mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
|
||||
}
|
||||
|
||||
// mTempDate will either be a site-supplied value, or today's date otherwise. CalendarView implementations can
|
||||
// crash if they're supplied an invalid date (i.e. a date not in the specified range), hence we need to set
|
||||
// a sensible default date here.
|
||||
if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
|
||||
mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
|
||||
}
|
||||
|
||||
// If we're displaying a date, the screen is wide enough
|
||||
// (and if we're using an SDK where the calendar view exists)
|
||||
// then display a calendar.
|
||||
if (mState == PickersState.DATE || mState == PickersState.DATETIME) {
|
||||
mCalendar = new CalendarView(context);
|
||||
mCalendar.setVisibility(GONE);
|
||||
|
||||
mCalendar.setFocusable(true);
|
||||
mCalendar.setFocusableInTouchMode(true);
|
||||
mCalendar.setMaxDate(mMaxDate.getTimeInMillis());
|
||||
mCalendar.setMinDate(mMinDate.getTimeInMillis());
|
||||
mCalendar.setDate(mTempDate.getTimeInMillis(), false, false);
|
||||
|
||||
mCalendar.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
|
||||
@Override
|
||||
public void onSelectedDayChange(
|
||||
CalendarView view, int year, int month, int monthDay) {
|
||||
mTempDate.set(year, month, monthDay);
|
||||
setDate(mTempDate);
|
||||
notifyDateChanged();
|
||||
}
|
||||
});
|
||||
|
||||
final int height;
|
||||
if (Versions.preLollipop) {
|
||||
// The 4.X version of CalendarView doesn't request any height, resulting in
|
||||
// the whole dialog not appearing unless we manually request height.
|
||||
height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());;
|
||||
} else {
|
||||
height = LayoutParams.WRAP_CONTENT;
|
||||
}
|
||||
|
||||
mPickers.addView(mCalendar, LayoutParams.MATCH_PARENT, height);
|
||||
|
||||
} else {
|
||||
// If the screen is more wide than high, we are displaying day and
|
||||
// time spinners, and if there is no calendar displayed, we should
|
||||
// display the fields in one row.
|
||||
if (mScreenWidth > mScreenHeight && mState == PickersState.DATETIME) {
|
||||
mPickers.setOrientation(LinearLayout.HORIZONTAL);
|
||||
}
|
||||
mCalendar = null;
|
||||
}
|
||||
|
||||
// Initialize all spinners.
|
||||
mDaySpinner = setupSpinner(R.id.day, 1,
|
||||
mTempDate.get(Calendar.DAY_OF_MONTH));
|
||||
mDaySpinner.setFormatter(TWO_DIGIT_FORMATTER);
|
||||
mDaySpinnerInput = (EditText) mDaySpinner.getChildAt(1);
|
||||
|
||||
mMonthSpinner = setupSpinner(R.id.month, 1,
|
||||
mTempDate.get(Calendar.MONTH) + 1); // Month is 0-based
|
||||
mMonthSpinner.setFormatter(TWO_DIGIT_FORMATTER);
|
||||
mMonthSpinner.setDisplayedValues(mShortMonths);
|
||||
mMonthSpinnerInput = (EditText) mMonthSpinner.getChildAt(1);
|
||||
|
||||
mWeekSpinner = setupSpinner(R.id.week, 1,
|
||||
mTempDate.get(Calendar.WEEK_OF_YEAR));
|
||||
mWeekSpinner.setFormatter(TWO_DIGIT_FORMATTER);
|
||||
mWeekSpinnerInput = (EditText) mWeekSpinner.getChildAt(1);
|
||||
|
||||
mYearSpinner = setupSpinner(R.id.year, DEFAULT_START_YEAR,
|
||||
DEFAULT_END_YEAR);
|
||||
mYearSpinnerInput = (EditText) mYearSpinner.getChildAt(1);
|
||||
|
||||
mAMPMSpinner = setupSpinner(R.id.ampm, 0, 1);
|
||||
mAMPMSpinner.setFormatter(TWO_DIGIT_FORMATTER);
|
||||
|
||||
if (mIs12HourMode) {
|
||||
mHourSpinner = setupSpinner(R.id.hour, 1, 12);
|
||||
mAMPMSpinnerInput = (EditText) mAMPMSpinner.getChildAt(1);
|
||||
mAMPMSpinner.setDisplayedValues(mShortAMPMs);
|
||||
} else {
|
||||
mHourSpinner = setupSpinner(R.id.hour, 0, 23);
|
||||
mAMPMSpinnerInput = null;
|
||||
}
|
||||
|
||||
mHourSpinner.setFormatter(TWO_DIGIT_FORMATTER);
|
||||
mHourSpinnerInput = (EditText) mHourSpinner.getChildAt(1);
|
||||
|
||||
mMinuteSpinner = setupSpinner(R.id.minute, 0, 59);
|
||||
mMinuteSpinner.setFormatter(TWO_DIGIT_FORMATTER);
|
||||
mMinuteSpinnerInput = (EditText) mMinuteSpinner.getChildAt(1);
|
||||
|
||||
// The order in which the spinners are displayed are locale-dependent
|
||||
reorderDateSpinners();
|
||||
|
||||
// Set the date to the initial date. Since this date can come from the user,
|
||||
// it can fire an exception (out-of-bound date)
|
||||
try {
|
||||
updateDate(mTempDate);
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
|
||||
// Display only the pickers needed for the current state.
|
||||
displayPickers();
|
||||
}
|
||||
|
||||
public NumberPicker setupSpinner(int id, int min, int max) {
|
||||
NumberPicker mSpinner = (NumberPicker) findViewById(id);
|
||||
mSpinner.setMinValue(min);
|
||||
mSpinner.setMaxValue(max);
|
||||
mSpinner.setOnValueChangedListener(mOnChangeListener);
|
||||
mSpinner.setOnLongPressUpdateInterval(100);
|
||||
return mSpinner;
|
||||
}
|
||||
|
||||
public long getTimeInMillis() {
|
||||
return mCurrentDate.getTimeInMillis();
|
||||
}
|
||||
|
||||
private void reorderDateSpinners() {
|
||||
mDateSpinners.removeAllViews();
|
||||
char[] order = DateFormat.getDateFormatOrder(getContext());
|
||||
final int spinnerCount = order.length;
|
||||
|
||||
for (int i = 0; i < spinnerCount; i++) {
|
||||
switch (order[i]) {
|
||||
case DATE_FORMAT_DAY:
|
||||
mDateSpinners.addView(mDaySpinner);
|
||||
break;
|
||||
case DATE_FORMAT_MONTH:
|
||||
mDateSpinners.addView(mMonthSpinner);
|
||||
break;
|
||||
case DATE_FORMAT_YEAR:
|
||||
mDateSpinners.addView(mYearSpinner);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
mDateSpinners.addView(mWeekSpinner);
|
||||
}
|
||||
|
||||
void setDate(Calendar calendar) {
|
||||
mCurrentDate = mTempDate;
|
||||
if (mCurrentDate.before(mMinDate)) {
|
||||
mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
|
||||
} else if (mCurrentDate.after(mMaxDate)) {
|
||||
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
|
||||
}
|
||||
}
|
||||
|
||||
void updateInputState() {
|
||||
InputMethodManager inputMethodManager = (InputMethodManager)
|
||||
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (mYearEnabled && inputMethodManager.isActive(mYearSpinnerInput)) {
|
||||
mYearSpinnerInput.clearFocus();
|
||||
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
} else if (mMonthEnabled && inputMethodManager.isActive(mMonthSpinnerInput)) {
|
||||
mMonthSpinnerInput.clearFocus();
|
||||
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
} else if (mDayEnabled && inputMethodManager.isActive(mDaySpinnerInput)) {
|
||||
mDaySpinnerInput.clearFocus();
|
||||
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
} else if (mHourEnabled && inputMethodManager.isActive(mHourSpinnerInput)) {
|
||||
mHourSpinnerInput.clearFocus();
|
||||
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
} else if (mMinuteEnabled && inputMethodManager.isActive(mMinuteSpinnerInput)) {
|
||||
mMinuteSpinnerInput.clearFocus();
|
||||
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
void updateSpinners() {
|
||||
if (mDayEnabled) {
|
||||
if (mCurrentDate.equals(mMinDate)) {
|
||||
mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
|
||||
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||
} else if (mCurrentDate.equals(mMaxDate)) {
|
||||
mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
|
||||
mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
|
||||
} else {
|
||||
mDaySpinner.setMinValue(1);
|
||||
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
|
||||
if (mWeekEnabled) {
|
||||
mWeekSpinner.setMinValue(1);
|
||||
mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR));
|
||||
mWeekSpinner.setValue(mCurrentDate.get(Calendar.WEEK_OF_YEAR));
|
||||
}
|
||||
|
||||
if (mMonthEnabled) {
|
||||
mMonthSpinner.setDisplayedValues(null);
|
||||
if (mCurrentDate.equals(mMinDate)) {
|
||||
mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
|
||||
mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
|
||||
} else if (mCurrentDate.equals(mMaxDate)) {
|
||||
mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
|
||||
mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
|
||||
} else {
|
||||
mMonthSpinner.setMinValue(Calendar.JANUARY);
|
||||
mMonthSpinner.setMaxValue(Calendar.DECEMBER);
|
||||
}
|
||||
|
||||
String[] displayedValues = Arrays.copyOfRange(mShortMonths,
|
||||
mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
|
||||
mMonthSpinner.setDisplayedValues(displayedValues);
|
||||
mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
|
||||
}
|
||||
|
||||
if (mYearEnabled) {
|
||||
mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
|
||||
mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
|
||||
mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
|
||||
}
|
||||
|
||||
if (mHourEnabled) {
|
||||
if (mIs12HourMode) {
|
||||
mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR));
|
||||
mAMPMSpinner.setValue(mCurrentDate.get(Calendar.AM_PM));
|
||||
mAMPMSpinner.setDisplayedValues(mShortAMPMs);
|
||||
} else {
|
||||
mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR_OF_DAY));
|
||||
}
|
||||
}
|
||||
if (mMinuteEnabled) {
|
||||
mMinuteSpinner.setValue(mCurrentDate.get(Calendar.MINUTE));
|
||||
}
|
||||
}
|
||||
|
||||
void updateCalendar() {
|
||||
if (mCalendarEnabled) {
|
||||
mCalendar.setDate(mCurrentDate.getTimeInMillis(), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
void notifyDateChanged() {
|
||||
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
|
||||
}
|
||||
|
||||
public void toggleCalendar(boolean shown) {
|
||||
if ((mState != PickersState.DATE && mState != PickersState.DATETIME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shown) {
|
||||
mCalendarEnabled = true;
|
||||
mCalendar.setVisibility(VISIBLE);
|
||||
setYearShown(false);
|
||||
setWeekShown(false);
|
||||
setMonthShown(false);
|
||||
setDayShown(false);
|
||||
} else {
|
||||
mCalendar.setVisibility(GONE);
|
||||
setYearShown(true);
|
||||
setMonthShown(true);
|
||||
setDayShown(true);
|
||||
mPickers.setOrientation(LinearLayout.HORIZONTAL);
|
||||
mCalendarEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setYearShown(boolean shown) {
|
||||
if (shown) {
|
||||
toggleCalendar(false);
|
||||
mYearSpinner.setVisibility(VISIBLE);
|
||||
mYearEnabled = true;
|
||||
} else {
|
||||
mYearSpinner.setVisibility(GONE);
|
||||
mYearEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setWeekShown(boolean shown) {
|
||||
if (shown) {
|
||||
toggleCalendar(false);
|
||||
mWeekSpinner.setVisibility(VISIBLE);
|
||||
mWeekEnabled = true;
|
||||
} else {
|
||||
mWeekSpinner.setVisibility(GONE);
|
||||
mWeekEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setMonthShown(boolean shown) {
|
||||
if (shown) {
|
||||
toggleCalendar(false);
|
||||
mMonthSpinner.setVisibility(VISIBLE);
|
||||
mMonthEnabled = true;
|
||||
} else {
|
||||
mMonthSpinner.setVisibility(GONE);
|
||||
mMonthEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setDayShown(boolean shown) {
|
||||
if (shown) {
|
||||
toggleCalendar(false);
|
||||
mDaySpinner.setVisibility(VISIBLE);
|
||||
mDayEnabled = true;
|
||||
} else {
|
||||
mDaySpinner.setVisibility(GONE);
|
||||
mDayEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void set12HourShown(boolean shown) {
|
||||
if (shown) {
|
||||
mAMPMSpinner.setVisibility(VISIBLE);
|
||||
} else {
|
||||
mAMPMSpinner.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setHourShown(boolean shown) {
|
||||
if (shown) {
|
||||
mHourSpinner.setVisibility(VISIBLE);
|
||||
mHourEnabled = true;
|
||||
} else {
|
||||
mHourSpinner.setVisibility(GONE);
|
||||
mAMPMSpinner.setVisibility(GONE);
|
||||
mTimeSpinners.setVisibility(GONE);
|
||||
mHourEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setMinuteShown(boolean shown) {
|
||||
if (shown) {
|
||||
mMinuteSpinner.setVisibility(VISIBLE);
|
||||
mTimeSpinners.findViewById(R.id.mincolon).setVisibility(VISIBLE);
|
||||
mMinuteEnabled = true;
|
||||
} else {
|
||||
mMinuteSpinner.setVisibility(GONE);
|
||||
mTimeSpinners.findViewById(R.id.mincolon).setVisibility(GONE);
|
||||
mMinuteEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setCurrentLocale(Locale locale) {
|
||||
if (locale.equals(mCurrentLocale)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentLocale = locale;
|
||||
mIs12HourMode = !DateFormat.is24HourFormat(getContext());
|
||||
mTempDate = getCalendarForLocale(mTempDate, locale);
|
||||
mMinDate = getCalendarForLocale(mMinDate, locale);
|
||||
mMaxDate = getCalendarForLocale(mMaxDate, locale);
|
||||
mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
|
||||
|
||||
mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
|
||||
|
||||
mShortAMPMs = new String[2];
|
||||
mShortAMPMs[0] = DateUtils.getAMPMString(Calendar.AM);
|
||||
mShortAMPMs[1] = DateUtils.getAMPMString(Calendar.PM);
|
||||
|
||||
mShortMonths = new String[mNumberOfMonths];
|
||||
for (int i = 0; i < mNumberOfMonths; i++) {
|
||||
mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
|
||||
DateUtils.LENGTH_MEDIUM);
|
||||
}
|
||||
}
|
||||
|
||||
private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
|
||||
if (oldCalendar == null) {
|
||||
return Calendar.getInstance(locale);
|
||||
}
|
||||
|
||||
final long currentTimeMillis = oldCalendar.getTimeInMillis();
|
||||
Calendar newCalendar = Calendar.getInstance(locale);
|
||||
newCalendar.setTimeInMillis(currentTimeMillis);
|
||||
return newCalendar;
|
||||
}
|
||||
|
||||
public void updateDate(Calendar calendar) {
|
||||
if (mCurrentDate.equals(calendar)) {
|
||||
return;
|
||||
}
|
||||
mCurrentDate.setTimeInMillis(calendar.getTimeInMillis());
|
||||
if (mCurrentDate.before(mMinDate)) {
|
||||
mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
|
||||
} else if (mCurrentDate.after(mMaxDate)) {
|
||||
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
|
||||
}
|
||||
updateSpinners();
|
||||
notifyDateChanged();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.util.Log;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.prompts.PromptInput;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class DefaultDoorHanger extends DoorHanger {
|
||||
private static final String LOGTAG = "GeckoDefaultDoorHanger";
|
||||
|
||||
private static int sSpinnerTextColor = -1;
|
||||
|
||||
private final TextView mMessage;
|
||||
private List<PromptInput> mInputs;
|
||||
private CheckBox mCheckBox;
|
||||
|
||||
public DefaultDoorHanger(Context context, DoorhangerConfig config, Type type) {
|
||||
super(context, config, type);
|
||||
|
||||
mMessage = (TextView) findViewById(R.id.doorhanger_message);
|
||||
|
||||
if (sSpinnerTextColor == -1) {
|
||||
sSpinnerTextColor = ContextCompat.getColor(context, R.color.text_color_primary_disable_only);
|
||||
}
|
||||
|
||||
switch (mType) {
|
||||
case GEOLOCATION:
|
||||
mIcon.setImageResource(R.drawable.location);
|
||||
mIcon.setVisibility(VISIBLE);
|
||||
break;
|
||||
|
||||
case DESKTOPNOTIFICATION2:
|
||||
mIcon.setImageResource(R.drawable.push_notification);
|
||||
mIcon.setVisibility(VISIBLE);
|
||||
break;
|
||||
}
|
||||
|
||||
loadConfig(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadConfig(DoorhangerConfig config) {
|
||||
final String message = config.getMessage();
|
||||
if (message != null) {
|
||||
setMessage(message);
|
||||
}
|
||||
|
||||
final JSONObject options = config.getOptions();
|
||||
if (options != null) {
|
||||
setOptions(options);
|
||||
}
|
||||
|
||||
final DoorhangerConfig.Link link = config.getLink();
|
||||
if (link != null) {
|
||||
addLink(link.label, link.url);
|
||||
}
|
||||
|
||||
addButtonsToLayout(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getContentResource() {
|
||||
return R.layout.default_doorhanger;
|
||||
}
|
||||
|
||||
private List<PromptInput> getInputs() {
|
||||
return mInputs;
|
||||
}
|
||||
|
||||
private CheckBox getCheckBox() {
|
||||
return mCheckBox;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOptions(final JSONObject options) {
|
||||
super.setOptions(options);
|
||||
|
||||
final JSONArray inputs = options.optJSONArray("inputs");
|
||||
if (inputs != null) {
|
||||
mInputs = new ArrayList<PromptInput>();
|
||||
|
||||
final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs);
|
||||
group.setVisibility(VISIBLE);
|
||||
|
||||
for (int i = 0; i < inputs.length(); i++) {
|
||||
try {
|
||||
PromptInput input = PromptInput.getInput(inputs.getJSONObject(i));
|
||||
mInputs.add(input);
|
||||
|
||||
final int padding = mResources.getDimensionPixelSize(R.dimen.doorhanger_section_padding_medium);
|
||||
View v = input.getView(getContext());
|
||||
styleInput(input, v);
|
||||
v.setPadding(0, 0, 0, padding);
|
||||
group.addView(v);
|
||||
} catch (JSONException ex) { }
|
||||
}
|
||||
}
|
||||
|
||||
final String checkBoxText = options.optString("checkbox");
|
||||
if (!TextUtils.isEmpty(checkBoxText)) {
|
||||
mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox);
|
||||
mCheckBox.setText(checkBoxText);
|
||||
if (options.has("checkboxState")) {
|
||||
final boolean checkBoxState = options.optBoolean("checkboxState");
|
||||
mCheckBox.setChecked(checkBoxState);
|
||||
}
|
||||
mCheckBox.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
|
||||
return new Button.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
|
||||
|
||||
final JSONObject response = new JSONObject();
|
||||
try {
|
||||
response.put("callback", id);
|
||||
|
||||
CheckBox checkBox = getCheckBox();
|
||||
// If the checkbox is being used, pass its value
|
||||
if (checkBox != null) {
|
||||
response.put("checked", checkBox.isChecked());
|
||||
}
|
||||
|
||||
List<PromptInput> doorHangerInputs = getInputs();
|
||||
if (doorHangerInputs != null) {
|
||||
JSONObject inputs = new JSONObject();
|
||||
for (PromptInput input : doorHangerInputs) {
|
||||
inputs.put(input.getId(), input.getValue());
|
||||
}
|
||||
response.put("inputs", inputs);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error creating onClick response", e);
|
||||
}
|
||||
|
||||
mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setMessage(String message) {
|
||||
Spanned markupMessage = Html.fromHtml(message);
|
||||
mMessage.setText(markupMessage);
|
||||
}
|
||||
|
||||
private void styleInput(PromptInput input, View view) {
|
||||
if (input instanceof PromptInput.MenulistInput) {
|
||||
styleDropdownInputs(input, view);
|
||||
}
|
||||
view.setPadding(0, 0, 0, mResources.getDimensionPixelSize(R.dimen.doorhanger_subsection_padding));
|
||||
}
|
||||
|
||||
private void styleDropdownInputs(PromptInput input, View view) {
|
||||
PromptInput.MenulistInput spinInput = (PromptInput.MenulistInput) input;
|
||||
|
||||
if (spinInput.textView != null) {
|
||||
spinInput.textView.setTextColor(sSpinnerTextColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,685 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.animation.AnimatorCompatHelper;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorListener;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SimpleItemAnimator;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This basically follows the approach taken by Wasabeef:
|
||||
* <a href="https://github.com/wasabeef/recyclerview-animators">https://github.com/wasabeef/recyclerview-animators</a>
|
||||
* based off of Android's DefaultItemAnimator from October 2016:
|
||||
* <a href="https://github.com/android/platform_frameworks_support/blob/432f3317f8a9b8cf98277938ea5df4021e983055/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java">
|
||||
* https://github.com/android/platform_frameworks_support/blob/432f3317f8a9b8cf98277938ea5df4021e983055/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java
|
||||
* </a>
|
||||
* <p>
|
||||
* Usage Notes:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>You <strong>must</strong> add a Default*VpaListener to your animate*Impl animation - the
|
||||
* listener takes care of animation bookkeeping.</li>
|
||||
* <li>You should call {@link #resetAnimation(RecyclerView.ViewHolder)} at some point in
|
||||
* preAnimate*Impl if you choose to proceed with the animation. Some animations will want to
|
||||
* know some or all of the current animation values for initializing their own animation
|
||||
* values before resetting the current animation, so this class does not provide the reset
|
||||
* service itself.</li>
|
||||
* <li>{@link #resetViewProperties(View)} is used to reset a view any time an animation ends or
|
||||
* gets canceled - you should redefine resetViewProperties if the version here doesn't reset
|
||||
* all of the properties you're animating.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class DefaultItemAnimatorBase extends SimpleItemAnimator {
|
||||
private List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();
|
||||
private List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
|
||||
private List<MoveInfo> pendingMoves = new ArrayList<>();
|
||||
private List<ChangeInfo> pendingChanges = new ArrayList<>();
|
||||
|
||||
private List<List<RecyclerView.ViewHolder>> additionsList = new ArrayList<>();
|
||||
private List<List<MoveInfo>> movesList = new ArrayList<>();
|
||||
private List<List<ChangeInfo>> changesList = new ArrayList<>();
|
||||
|
||||
private List<RecyclerView.ViewHolder> addAnimations = new ArrayList<>();
|
||||
private List<RecyclerView.ViewHolder> moveAnimations = new ArrayList<>();
|
||||
private List<RecyclerView.ViewHolder> removeAnimations = new ArrayList<>();
|
||||
private List<RecyclerView.ViewHolder> changeAnimations = new ArrayList<>();
|
||||
|
||||
protected static class MoveInfo {
|
||||
public RecyclerView.ViewHolder holder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
public MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
this.holder = holder;
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class ChangeInfo {
|
||||
public RecyclerView.ViewHolder oldHolder, newHolder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
|
||||
this.oldHolder = oldHolder;
|
||||
this.newHolder = newHolder;
|
||||
}
|
||||
|
||||
public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
this(oldHolder, newHolder);
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangeInfo{" +
|
||||
"oldHolder=" + oldHolder +
|
||||
", newHolder=" + newHolder +
|
||||
", fromX=" + fromX +
|
||||
", fromY=" + fromY +
|
||||
", toX=" + toX +
|
||||
", toY=" + toY +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runPendingAnimations() {
|
||||
final boolean removalsPending = !pendingRemovals.isEmpty();
|
||||
final boolean movesPending = !pendingMoves.isEmpty();
|
||||
final boolean changesPending = !pendingChanges.isEmpty();
|
||||
final boolean additionsPending = !pendingAdditions.isEmpty();
|
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||
return;
|
||||
}
|
||||
// First, remove stuff.
|
||||
for (final RecyclerView.ViewHolder holder : pendingRemovals) {
|
||||
animateRemoveImpl(holder);
|
||||
}
|
||||
pendingRemovals.clear();
|
||||
// Next, move stuff.
|
||||
if (movesPending) {
|
||||
final List<MoveInfo> moves = new ArrayList<>();
|
||||
moves.addAll(pendingMoves);
|
||||
movesList.add(moves);
|
||||
pendingMoves.clear();
|
||||
final Runnable mover = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (final MoveInfo moveInfo : moves) {
|
||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
||||
moveInfo.toX, moveInfo.toY);
|
||||
}
|
||||
moves.clear();
|
||||
movesList.remove(moves);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
final View view = moves.get(0).holder.itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
|
||||
} else {
|
||||
mover.run();
|
||||
}
|
||||
}
|
||||
// Next, change stuff, to run in parallel with move animations.
|
||||
if (changesPending) {
|
||||
final List<ChangeInfo> changes = new ArrayList<>();
|
||||
changes.addAll(pendingChanges);
|
||||
changesList.add(changes);
|
||||
pendingChanges.clear();
|
||||
final Runnable changer = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (final ChangeInfo change : changes) {
|
||||
animateChangeImpl(change);
|
||||
}
|
||||
changes.clear();
|
||||
changesList.remove(changes);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
|
||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
|
||||
} else {
|
||||
changer.run();
|
||||
}
|
||||
}
|
||||
// Next, add stuff.
|
||||
if (additionsPending) {
|
||||
final List<RecyclerView.ViewHolder> additions = new ArrayList<>();
|
||||
additions.addAll(pendingAdditions);
|
||||
additionsList.add(additions);
|
||||
pendingAdditions.clear();
|
||||
final Runnable adder = new Runnable() {
|
||||
public void run() {
|
||||
for (final RecyclerView.ViewHolder holder : additions) {
|
||||
animateAddImpl(holder);
|
||||
}
|
||||
additions.clear();
|
||||
additionsList.remove(additions);
|
||||
}
|
||||
};
|
||||
if (removalsPending || movesPending || changesPending) {
|
||||
final long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
||||
final long moveDuration = movesPending ? getMoveDuration() : 0;
|
||||
final long changeDuration = changesPending ? getChangeDuration() : 0;
|
||||
final long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
||||
final View view = additions.get(0).itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
|
||||
} else {
|
||||
adder.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
|
||||
if (!preAnimateRemoveImpl(holder)) {
|
||||
dispatchRemoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
pendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
|
||||
ViewCompat.animate(holder.itemView)
|
||||
.setDuration(getRemoveDuration())
|
||||
.alpha(0)
|
||||
.setListener(new DefaultRemoveVpaListener(holder))
|
||||
.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
|
||||
if (!preAnimateAddImpl(holder)) {
|
||||
dispatchAddFinished(holder);
|
||||
return false;
|
||||
}
|
||||
pendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean preAnimateAddImpl(RecyclerView.ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
holder.itemView.setAlpha(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
|
||||
ViewCompat.animate(holder.itemView)
|
||||
.setDuration(getAddDuration())
|
||||
.alpha(1)
|
||||
.setListener(new DefaultAddVpaListener(holder))
|
||||
.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(final RecyclerView.ViewHolder holder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
fromX += ViewCompat.getTranslationX(holder.itemView);
|
||||
fromY += ViewCompat.getTranslationY(holder.itemView);
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
resetAnimation(holder);
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, -deltaX);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, -deltaY);
|
||||
}
|
||||
pendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void animateMoveImpl(final RecyclerView.ViewHolder holder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.animate(view).translationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.animate(view).translationY(0);
|
||||
}
|
||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
moveAnimations.add(holder);
|
||||
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchMoveStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
resetViewProperties(view);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchMoveFinished(holder);
|
||||
moveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
if (oldHolder == newHolder) {
|
||||
// Don't know how to run change animations when the same view holder is re-used.
|
||||
// Run a move animation to handle position changes (if there are any).
|
||||
if (fromX != toX || fromY != toY) {
|
||||
// *Don't* call dispatchChangeFinished here, it leads to unbalanced isRecyclable calls.
|
||||
return animateMove(oldHolder, fromX, fromY, toX, toY);
|
||||
}
|
||||
dispatchChangeFinished(oldHolder, true);
|
||||
return false;
|
||||
}
|
||||
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
|
||||
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
|
||||
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
|
||||
resetAnimation(oldHolder);
|
||||
final int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||
final int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||
// Recover previous translation state after ending animation.
|
||||
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
|
||||
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
|
||||
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
|
||||
if (newHolder != null) {
|
||||
// Carry over translation values.
|
||||
resetAnimation(newHolder);
|
||||
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
|
||||
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
|
||||
ViewCompat.setAlpha(newHolder.itemView, 0);
|
||||
}
|
||||
pendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||
final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
|
||||
final View view = holder == null ? null : holder.itemView;
|
||||
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
if (view != null) {
|
||||
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
|
||||
getChangeDuration());
|
||||
changeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
oldViewAnim.setListener(null);
|
||||
resetViewProperties(view);
|
||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||
changeAnimations.remove(changeInfo.oldHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
if (newView != null) {
|
||||
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
|
||||
changeAnimations.add(changeInfo.newHolder);
|
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
|
||||
alpha(1).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
newViewAnimation.setListener(null);
|
||||
resetViewProperties(view);
|
||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||
changeAnimations.remove(changeInfo.newHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimation(List<ChangeInfo> infoList, RecyclerView.ViewHolder item) {
|
||||
for (int i = infoList.size() - 1; i >= 0; i--) {
|
||||
final ChangeInfo changeInfo = infoList.get(i);
|
||||
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||
infoList.remove(changeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
|
||||
if (changeInfo.oldHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
||||
}
|
||||
if (changeInfo.newHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
|
||||
boolean oldItem = false;
|
||||
if (changeInfo.newHolder == item) {
|
||||
changeInfo.newHolder = null;
|
||||
} else if (changeInfo.oldHolder == item) {
|
||||
changeInfo.oldHolder = null;
|
||||
oldItem = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
resetViewProperties(item.itemView);
|
||||
dispatchChangeFinished(item, oldItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to reset all properties possibly animated by any and all defined animations.
|
||||
*/
|
||||
protected void resetViewProperties(View view) {
|
||||
view.setTranslationX(0);
|
||||
view.setTranslationY(0);
|
||||
view.setAlpha(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimation(RecyclerView.ViewHolder item) {
|
||||
|
||||
final View view = item.itemView;
|
||||
// This calls dispatch*Finished, resets view properties, and removes item from current
|
||||
// animations list if the view is currently being animated.
|
||||
ViewCompat.animate(view).cancel();
|
||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = pendingMoves.size() - 1; i >= 0; i--) {
|
||||
final MoveInfo moveInfo = pendingMoves.get(i);
|
||||
if (moveInfo.holder == item) {
|
||||
resetViewProperties(view);
|
||||
dispatchMoveFinished(item);
|
||||
pendingMoves.remove(i);
|
||||
}
|
||||
}
|
||||
endChangeAnimation(pendingChanges, item);
|
||||
if (pendingRemovals.remove(item)) {
|
||||
resetViewProperties(view);
|
||||
dispatchRemoveFinished(item);
|
||||
}
|
||||
if (pendingAdditions.remove(item)) {
|
||||
resetViewProperties(view);
|
||||
dispatchAddFinished(item);
|
||||
}
|
||||
|
||||
for (int i = changesList.size() - 1; i >= 0; i--) {
|
||||
final List<ChangeInfo> changes = changesList.get(i);
|
||||
endChangeAnimation(changes, item);
|
||||
if (changes.isEmpty()) {
|
||||
changesList.remove(i);
|
||||
}
|
||||
}
|
||||
for (int i = movesList.size() - 1; i >= 0; i--) {
|
||||
final List<MoveInfo> moves = movesList.get(i);
|
||||
for (int j = moves.size() - 1; j >= 0; j--) {
|
||||
final MoveInfo moveInfo = moves.get(j);
|
||||
if (moveInfo.holder == item) {
|
||||
resetViewProperties(view);
|
||||
dispatchMoveFinished(item);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
movesList.remove(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = additionsList.size() - 1; i >= 0; i--) {
|
||||
final List<RecyclerView.ViewHolder> additions = additionsList.get(i);
|
||||
if (additions.remove(item)) {
|
||||
resetViewProperties(view);
|
||||
dispatchAddFinished(item);
|
||||
if (additions.isEmpty()) {
|
||||
additionsList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
|
||||
protected void resetAnimation(RecyclerView.ViewHolder holder) {
|
||||
AnimatorCompatHelper.clearInterpolator(holder.itemView);
|
||||
endAnimation(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (!pendingAdditions.isEmpty() ||
|
||||
!pendingChanges.isEmpty() ||
|
||||
!pendingMoves.isEmpty() ||
|
||||
!pendingRemovals.isEmpty() ||
|
||||
!moveAnimations.isEmpty() ||
|
||||
!removeAnimations.isEmpty() ||
|
||||
!addAnimations.isEmpty() ||
|
||||
!changeAnimations.isEmpty() ||
|
||||
!movesList.isEmpty() ||
|
||||
!additionsList.isEmpty() ||
|
||||
!changesList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of currently pending and running animations. If there are none
|
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||
* listeners.
|
||||
*/
|
||||
protected void dispatchFinishedWhenDone() {
|
||||
if (!isRunning()) {
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimations() {
|
||||
int count = pendingMoves.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
final MoveInfo item = pendingMoves.get(i);
|
||||
resetViewProperties(item.holder.itemView);
|
||||
dispatchMoveFinished(item.holder);
|
||||
pendingMoves.remove(i);
|
||||
}
|
||||
count = pendingRemovals.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
final RecyclerView.ViewHolder item = pendingRemovals.get(i);
|
||||
resetViewProperties(item.itemView);
|
||||
dispatchRemoveFinished(item);
|
||||
pendingRemovals.remove(i);
|
||||
}
|
||||
count = pendingAdditions.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
final RecyclerView.ViewHolder item = pendingAdditions.get(i);
|
||||
resetViewProperties(item.itemView);
|
||||
dispatchAddFinished(item);
|
||||
pendingAdditions.remove(i);
|
||||
}
|
||||
count = pendingChanges.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
endChangeAnimationIfNecessary(pendingChanges.get(i));
|
||||
}
|
||||
pendingChanges.clear();
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int listCount = movesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
final List<MoveInfo> moves = movesList.get(i);
|
||||
count = moves.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
final MoveInfo moveInfo = moves.get(j);
|
||||
final RecyclerView.ViewHolder item = moveInfo.holder;
|
||||
resetViewProperties(item.itemView);
|
||||
dispatchMoveFinished(item);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
movesList.remove(moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = additionsList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
final List<RecyclerView.ViewHolder> additions = additionsList.get(i);
|
||||
count = additions.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
final RecyclerView.ViewHolder item = additions.get(j);
|
||||
resetViewProperties(item.itemView);
|
||||
dispatchAddFinished(item);
|
||||
additions.remove(j);
|
||||
if (additions.isEmpty()) {
|
||||
additionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = changesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
final List<ChangeInfo> changes = changesList.get(i);
|
||||
count = changes.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
endChangeAnimationIfNecessary(changes.get(j));
|
||||
if (changes.isEmpty()) {
|
||||
changesList.remove(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelAll(removeAnimations);
|
||||
cancelAll(moveAnimations);
|
||||
cancelAll(addAnimations);
|
||||
cancelAll(changeAnimations);
|
||||
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
|
||||
public void cancelAll(List<RecyclerView.ViewHolder> viewHolders) {
|
||||
for (int i = viewHolders.size() - 1; i >= 0; i--) {
|
||||
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
|
||||
* When this is the case:
|
||||
* <ul>
|
||||
* <li>If you override
|
||||
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
|
||||
* both ViewHolder arguments will be the same instance.
|
||||
* </li>
|
||||
* <li>
|
||||
* If you are not overriding
|
||||
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
|
||||
* then DefaultItemAnimator will call
|
||||
* {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and run a move animation
|
||||
* instead.
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull List<Object> payloads) {
|
||||
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
|
||||
}
|
||||
|
||||
private class VpaListenerAdapter implements ViewPropertyAnimatorListener {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {}
|
||||
|
||||
// Note that onAnimationEnd is called (in addition to OnAnimationCancel) whenever an
|
||||
// animation is canceled.
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
resetViewProperties(view);
|
||||
view.animate().setListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {}
|
||||
}
|
||||
|
||||
protected class DefaultRemoveVpaListener extends VpaListenerAdapter {
|
||||
private final RecyclerView.ViewHolder viewHolder;
|
||||
|
||||
public DefaultRemoveVpaListener(final RecyclerView.ViewHolder holder) {
|
||||
viewHolder = holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
removeAnimations.add(viewHolder);
|
||||
dispatchRemoveStarting(viewHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
removeAnimations.remove(viewHolder);
|
||||
dispatchRemoveFinished(viewHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
super.onAnimationEnd(view);
|
||||
}
|
||||
}
|
||||
|
||||
protected class DefaultAddVpaListener extends VpaListenerAdapter {
|
||||
private final RecyclerView.ViewHolder viewHolder;
|
||||
|
||||
public DefaultAddVpaListener(final RecyclerView.ViewHolder holder) {
|
||||
viewHolder = holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
addAnimations.add(viewHolder);
|
||||
dispatchAddStarting(viewHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
addAnimations.remove(viewHolder);
|
||||
dispatchAddFinished(viewHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
super.onAnimationEnd(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class DoorHanger extends LinearLayout {
|
||||
|
||||
public static DoorHanger Get(Context context, DoorhangerConfig config) {
|
||||
final Type type = config.getType();
|
||||
switch (type) {
|
||||
case LOGIN:
|
||||
return new LoginDoorHanger(context, config);
|
||||
case TRACKING:
|
||||
return new ContentSecurityDoorHanger(context, config, type);
|
||||
}
|
||||
return new DefaultDoorHanger(context, config, type);
|
||||
}
|
||||
|
||||
// Doorhanger types created from Gecko are checked against enum strings to determine type.
|
||||
public static enum Type { DEFAULT, LOGIN, TRACKING, GEOLOCATION, DESKTOPNOTIFICATION2, WEBRTC, VIBRATION }
|
||||
|
||||
public interface OnButtonClickListener {
|
||||
public void onButtonClick(JSONObject response, DoorHanger doorhanger);
|
||||
}
|
||||
|
||||
private static final String LOGTAG = "GeckoDoorHanger";
|
||||
|
||||
// Divider between doorhangers.
|
||||
private final View mDivider;
|
||||
|
||||
private final Button mNegativeButton;
|
||||
private final Button mPositiveButton;
|
||||
protected final OnButtonClickListener mOnButtonClickListener;
|
||||
|
||||
// The tab this doorhanger is associated with.
|
||||
private final int mTabId;
|
||||
|
||||
// DoorHanger identifier.
|
||||
private final String mIdentifier;
|
||||
|
||||
protected final Type mType;
|
||||
|
||||
protected final ImageView mIcon;
|
||||
protected final TextView mLink;
|
||||
protected final TextView mDoorhangerTitle;
|
||||
|
||||
protected final Context mContext;
|
||||
protected final Resources mResources;
|
||||
|
||||
protected int mDividerColor;
|
||||
|
||||
protected boolean mPersistWhileVisible;
|
||||
protected int mPersistenceCount;
|
||||
protected long mTimeout;
|
||||
|
||||
protected DoorHanger(Context context, DoorhangerConfig config, Type type) {
|
||||
super(context);
|
||||
|
||||
mContext = context;
|
||||
mResources = context.getResources();
|
||||
mTabId = config.getTabId();
|
||||
mIdentifier = config.getId();
|
||||
mType = type;
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.doorhanger, this);
|
||||
setOrientation(VERTICAL);
|
||||
|
||||
mDivider = findViewById(R.id.divider_doorhanger);
|
||||
mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
|
||||
mLink = (TextView) findViewById(R.id.doorhanger_link);
|
||||
mDoorhangerTitle = (TextView) findViewById(R.id.doorhanger_title);
|
||||
|
||||
mNegativeButton = (Button) findViewById(R.id.doorhanger_button_negative);
|
||||
mPositiveButton = (Button) findViewById(R.id.doorhanger_button_positive);
|
||||
mOnButtonClickListener = config.getButtonClickListener();
|
||||
|
||||
mDividerColor = ContextCompat.getColor(context, R.color.toolbar_divider_grey);
|
||||
|
||||
final ViewStub contentStub = (ViewStub) findViewById(R.id.content);
|
||||
contentStub.setLayoutResource(getContentResource());
|
||||
contentStub.inflate();
|
||||
|
||||
final String typeExtra = mType.toString().toLowerCase(Locale.US);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.DOORHANGER, typeExtra);
|
||||
}
|
||||
|
||||
protected abstract int getContentResource();
|
||||
|
||||
protected abstract void loadConfig(DoorhangerConfig config);
|
||||
|
||||
protected void setOptions(final JSONObject options) {
|
||||
final int persistence = options.optInt("persistence");
|
||||
if (persistence > 0) {
|
||||
mPersistenceCount = persistence;
|
||||
}
|
||||
|
||||
mPersistWhileVisible = options.optBoolean("persistWhileVisible");
|
||||
|
||||
final long timeout = options.optLong("timeout");
|
||||
if (timeout > 0) {
|
||||
mTimeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
protected void addButtonsToLayout(DoorhangerConfig config) {
|
||||
final DoorhangerConfig.ButtonConfig negativeButtonConfig = config.getNegativeButtonConfig();
|
||||
final DoorhangerConfig.ButtonConfig positiveButtonConfig = config.getPositiveButtonConfig();
|
||||
|
||||
if (negativeButtonConfig != null) {
|
||||
mNegativeButton.setText(negativeButtonConfig.label);
|
||||
mNegativeButton.setOnClickListener(makeOnButtonClickListener(negativeButtonConfig.callback, "negative"));
|
||||
mNegativeButton.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (positiveButtonConfig != null) {
|
||||
mPositiveButton.setText(positiveButtonConfig.label);
|
||||
mPositiveButton.setOnClickListener(makeOnButtonClickListener(positiveButtonConfig.callback, "positive"));
|
||||
mPositiveButton.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public int getTabId() {
|
||||
return mTabId;
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
return mIdentifier;
|
||||
}
|
||||
|
||||
public void showDivider() {
|
||||
mDivider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void hideDivider() {
|
||||
mDivider.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setIcon(int resId) {
|
||||
mIcon.setImageResource(resId);
|
||||
mIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
protected void addLink(String label, final String url) {
|
||||
mLink.setText(label);
|
||||
mLink.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
final String typeExtra = mType.toString().toLowerCase(Locale.US);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.DOORHANGER, typeExtra);
|
||||
Tabs.getInstance().loadUrlInTab(url);
|
||||
}
|
||||
});
|
||||
mLink.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
protected abstract OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra);
|
||||
|
||||
/*
|
||||
* Checks with persistence and timeout options to see if it's okay to remove a doorhanger.
|
||||
*
|
||||
* @param isShowing Whether or not this doorhanger is currently visible to the user.
|
||||
* (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden)
|
||||
*/
|
||||
public boolean shouldRemove(boolean isShowing) {
|
||||
if (mPersistWhileVisible && isShowing) {
|
||||
// We still want to decrement mPersistence, even if the popup is showing
|
||||
if (mPersistenceCount != 0)
|
||||
mPersistenceCount--;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If persistence is set to -1, the doorhanger will never be
|
||||
// automatically removed.
|
||||
if (mPersistenceCount != 0) {
|
||||
mPersistenceCount--;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() <= mTimeout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void showTitle(Bitmap favicon, String title) {
|
||||
mDoorhangerTitle.setText(title);
|
||||
mDoorhangerTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(getResources(), favicon), null, null, null);
|
||||
if (favicon != null) {
|
||||
mDoorhangerTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
|
||||
}
|
||||
mDoorhangerTitle.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
public void hideTitle() {
|
||||
mDoorhangerTitle.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.util.Log;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import org.mozilla.gecko.widget.DoorHanger.Type;
|
||||
|
||||
public class DoorhangerConfig {
|
||||
|
||||
public static class Link {
|
||||
public final String label;
|
||||
public final String url;
|
||||
|
||||
private Link(String label, String url) {
|
||||
this.label = label;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ButtonConfig {
|
||||
public final String label;
|
||||
public final int callback;
|
||||
|
||||
public ButtonConfig(String label, int callback) {
|
||||
this.label = label;
|
||||
this.callback = callback;
|
||||
}
|
||||
}
|
||||
private static final String LOGTAG = "DoorhangerConfig";
|
||||
|
||||
private final int tabId;
|
||||
private final String id;
|
||||
private final DoorHanger.OnButtonClickListener buttonClickListener;
|
||||
private final DoorHanger.Type type;
|
||||
private String message;
|
||||
private JSONObject options;
|
||||
private Link link;
|
||||
private ButtonConfig positiveButtonConfig;
|
||||
private ButtonConfig negativeButtonConfig;
|
||||
|
||||
public DoorhangerConfig(Type type, DoorHanger.OnButtonClickListener listener) {
|
||||
// XXX: This should only be used by SiteIdentityPopup doorhangers which
|
||||
// don't need tab or id references, until bug 1141904 unifies doorhangers.
|
||||
|
||||
this(-1, null, type, listener);
|
||||
}
|
||||
|
||||
public DoorhangerConfig(int tabId, String id, DoorHanger.Type type, DoorHanger.OnButtonClickListener buttonClickListener) {
|
||||
this.tabId = tabId;
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.buttonClickListener = buttonClickListener;
|
||||
}
|
||||
|
||||
public int getTabId() {
|
||||
return tabId;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setOptions(JSONObject options) {
|
||||
this.options = options;
|
||||
|
||||
// Set link if there is a link provided in options.
|
||||
final JSONObject linkObj = options.optJSONObject("link");
|
||||
if (linkObj != null) {
|
||||
try {
|
||||
setLink(linkObj.getString("label"), linkObj.getString("url"));
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Malformed link object in options");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JSONObject getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public void setButton(String label, int callbackId, boolean isPositive) {
|
||||
final ButtonConfig buttonConfig = new ButtonConfig(label, callbackId);
|
||||
if (isPositive) {
|
||||
positiveButtonConfig = buttonConfig;
|
||||
} else {
|
||||
negativeButtonConfig = buttonConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public ButtonConfig getPositiveButtonConfig() {
|
||||
return positiveButtonConfig;
|
||||
}
|
||||
|
||||
public ButtonConfig getNegativeButtonConfig() {
|
||||
return negativeButtonConfig;
|
||||
}
|
||||
|
||||
public DoorHanger.OnButtonClickListener getButtonClickListener() {
|
||||
return this.buttonClickListener;
|
||||
}
|
||||
|
||||
public void setLink(String label, String url) {
|
||||
this.link = new Link(label, url);
|
||||
}
|
||||
|
||||
public Link getLink() {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* Text view that correctly handles maxLines and ellipsizing for Android < 2.3.
|
||||
*/
|
||||
public class EllipsisTextView extends TextView {
|
||||
private final String ellipsis;
|
||||
|
||||
private final int maxLines;
|
||||
private CharSequence originalText;
|
||||
|
||||
public EllipsisTextView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public EllipsisTextView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, android.R.attr.textViewStyle);
|
||||
}
|
||||
|
||||
public EllipsisTextView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
ellipsis = getResources().getString(R.string.ellipsis);
|
||||
|
||||
TypedArray a = context.getTheme()
|
||||
.obtainStyledAttributes(attrs, R.styleable.EllipsisTextView, 0, 0);
|
||||
maxLines = a.getInteger(R.styleable.EllipsisTextView_ellipsizeAtLine, 1);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
public void setOriginalText(CharSequence text) {
|
||||
originalText = text;
|
||||
setText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
// There is extra space, start over with the original text
|
||||
if (getLineCount() < maxLines) {
|
||||
setText(originalText);
|
||||
}
|
||||
|
||||
// If we are over the max line attribute, ellipsize
|
||||
if (getLineCount() > maxLines) {
|
||||
final int endIndex = getLayout().getLineEnd(maxLines - 1) - 1 - ellipsis.length();
|
||||
final String text = getText().subSequence(0, endIndex) + ellipsis;
|
||||
// Make sure that we don't change originalText
|
||||
setText(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.ActivityHandlerHelper;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tab;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A DialogFragment to contain a dialog that appears when the user clicks an Intent:// URI during private browsing. The
|
||||
* dialog appears to notify the user that a clicked link will open in an external application, potentially leaking their
|
||||
* browsing history.
|
||||
*/
|
||||
public class ExternalIntentDuringPrivateBrowsingPromptFragment extends DialogFragment {
|
||||
private static final String LOGTAG = ExternalIntentDuringPrivateBrowsingPromptFragment.class.getSimpleName();
|
||||
private static final String FRAGMENT_TAG = "ExternalIntentPB";
|
||||
|
||||
private static final String KEY_APPLICATION_NAME = "matchingApplicationName";
|
||||
private static final String KEY_INTENT = "intent";
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(final Bundle savedInstanceState) {
|
||||
final Bundle args = getArguments();
|
||||
final CharSequence matchingApplicationName = args.getCharSequence(KEY_APPLICATION_NAME);
|
||||
final Intent intent = args.getParcelable(KEY_INTENT);
|
||||
|
||||
final Context context = getActivity();
|
||||
final String promptMessage = context.getString(R.string.intent_uri_private_browsing_prompt, matchingApplicationName);
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setMessage(promptMessage)
|
||||
.setTitle(intent.getDataString())
|
||||
.setPositiveButton(R.string.button_yes, new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
context.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.button_no, null /* we do nothing if the user rejects */ );
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
GeckoApplication.watchReference(getActivity(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the Activity is started or a dialog is shown. false if the Activity fails to start.
|
||||
*/
|
||||
public static boolean showDialogOrAndroidChooser(final Context context, final FragmentManager fragmentManager,
|
||||
final Intent intent) {
|
||||
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
|
||||
if (selectedTab == null || !selectedTab.isPrivate()) {
|
||||
return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
|
||||
}
|
||||
|
||||
final PackageManager pm = context.getPackageManager();
|
||||
final List<ResolveInfo> matchingActivities = pm.queryIntentActivities(intent, 0);
|
||||
if (matchingActivities.size() == 1) {
|
||||
final ExternalIntentDuringPrivateBrowsingPromptFragment fragment = new ExternalIntentDuringPrivateBrowsingPromptFragment();
|
||||
|
||||
final Bundle args = new Bundle(2);
|
||||
args.putCharSequence(KEY_APPLICATION_NAME, matchingActivities.get(0).loadLabel(pm));
|
||||
args.putParcelable(KEY_INTENT, intent);
|
||||
fragment.setArguments(args);
|
||||
|
||||
fragment.show(fragmentManager, FRAGMENT_TAG);
|
||||
// We don't know the results of the user interaction with the fragment so just return true.
|
||||
return true;
|
||||
} else if (matchingActivities.size() > 1) {
|
||||
// We want to show the Android Intent Chooser. However, we have no way of distinguishing regular tabs from
|
||||
// private tabs to the chooser. Thus, if a user chooses "Always" in regular browsing mode, the chooser will
|
||||
// not be shown and the URL will be opened. Therefore we explicitly show the chooser (which notably does not
|
||||
// have an "Always" option).
|
||||
final String androidChooserTitle =
|
||||
context.getResources().getString(R.string.intent_uri_private_browsing_multiple_match_title);
|
||||
final Intent chooserIntent = Intent.createChooser(intent, androidChooserTitle);
|
||||
return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, chooserIntent);
|
||||
} else {
|
||||
// Normally, we show about:neterror when an Intent does not resolve
|
||||
// but we don't have the references here to do that so log instead.
|
||||
Log.w(LOGTAG, "showDialogOrAndroidChooser unexpectedly called with Intent that does not resolve");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Shader;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Fades the end of the text by gecko:fadeWidth amount,
|
||||
* if the text is too long and requires an ellipsis.
|
||||
*
|
||||
* This implementation is an improvement over Android's built-in fadingEdge
|
||||
* but potentially slower than the {@link org.mozilla.gecko.widget.FadedSingleColorTextView}.
|
||||
* It works for text of multiple colors but only one background color. It works by
|
||||
* drawing a gradient rectangle with the background color over the text, fading it out.
|
||||
*/
|
||||
public class FadedMultiColorTextView extends FadedTextView {
|
||||
private final ColorStateList fadeBackgroundColorList;
|
||||
|
||||
private final Paint fadePaint;
|
||||
private FadedTextGradient backgroundGradient;
|
||||
|
||||
public FadedMultiColorTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
fadePaint = new Paint();
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedMultiColorTextView);
|
||||
fadeBackgroundColorList =
|
||||
a.getColorStateList(R.styleable.FadedMultiColorTextView_fadeBackgroundColor);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
final boolean needsEllipsis = needsEllipsis();
|
||||
if (needsEllipsis) {
|
||||
final int right = getWidth() - getCompoundPaddingRight();
|
||||
final float left = right - fadeWidth;
|
||||
|
||||
updateGradientShader(needsEllipsis, right);
|
||||
|
||||
// Shrink height of gradient to prevent it overlaying parent view border.
|
||||
// The shrunk size just nee to cover the text itself.
|
||||
final float density = getResources().getDisplayMetrics().density;
|
||||
final float h = Math.abs(fadePaint.getFontMetrics().top) + 1;
|
||||
final float l = fadePaint.getFontMetrics().bottom + 1;
|
||||
final float top = getBaseline() - h * density;
|
||||
final float bottom = getBaseline() + l * density;
|
||||
|
||||
canvas.drawRect(left, top, right, bottom, fadePaint);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGradientShader(final boolean needsEllipsis, final int gradientEndRight) {
|
||||
final int backgroundColor =
|
||||
fadeBackgroundColorList.getColorForState(getDrawableState(), Color.RED);
|
||||
|
||||
final boolean needsNewGradient = (backgroundGradient == null ||
|
||||
backgroundGradient.getBackgroundColor() != backgroundColor ||
|
||||
backgroundGradient.getEndRight() != gradientEndRight);
|
||||
|
||||
if (needsEllipsis && needsNewGradient) {
|
||||
backgroundGradient = new FadedTextGradient(gradientEndRight, fadeWidth, backgroundColor);
|
||||
fadePaint.setShader(backgroundGradient);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FadedTextGradient extends LinearGradient {
|
||||
private final int endRight;
|
||||
private final int backgroundColor;
|
||||
|
||||
public FadedTextGradient(final int gradientEndRight, final int fadeWidth,
|
||||
final int backgroundColor) {
|
||||
super(gradientEndRight - fadeWidth, 0, gradientEndRight, 0,
|
||||
getColorWithZeroedAlpha(backgroundColor), backgroundColor, Shader.TileMode.CLAMP);
|
||||
|
||||
this.endRight = gradientEndRight;
|
||||
this.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
private static int getColorWithZeroedAlpha(final int color) {
|
||||
return Color.argb(0, Color.red(color), Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
public int getEndRight() {
|
||||
return endRight;
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Shader;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Fades the end of the text by gecko:fadeWidth amount,
|
||||
* if the text is too long and requires an ellipsis.
|
||||
*
|
||||
* This implementation is an improvement over Android's built-in fadingEdge
|
||||
* and the fastest of Fennec's implementations. However, it only works for
|
||||
* text of one color. It works by applying a linear gradient directly to the text.
|
||||
*/
|
||||
public class FadedSingleColorTextView extends FadedTextView {
|
||||
// Shader for the fading edge.
|
||||
private FadedTextGradient mTextGradient;
|
||||
|
||||
public FadedSingleColorTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private void updateGradientShader() {
|
||||
final int color = getCurrentTextColor();
|
||||
final int width = getAvailableWidth();
|
||||
|
||||
final boolean needsNewGradient = (mTextGradient == null ||
|
||||
mTextGradient.getColor() != color ||
|
||||
mTextGradient.getWidth() != width);
|
||||
|
||||
final boolean needsEllipsis = needsEllipsis();
|
||||
if (needsEllipsis && needsNewGradient) {
|
||||
mTextGradient = new FadedTextGradient(width, fadeWidth, color);
|
||||
}
|
||||
|
||||
getPaint().setShader(needsEllipsis ? mTextGradient : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
updateGradientShader();
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
private static class FadedTextGradient extends LinearGradient {
|
||||
private final int mWidth;
|
||||
private final int mColor;
|
||||
|
||||
public FadedTextGradient(int width, int fadeWidth, int color) {
|
||||
super(0, 0, width, 0,
|
||||
new int[] { color, color, 0x0 },
|
||||
new float[] { 0, ((float) (width - fadeWidth) / width), 1.0f },
|
||||
Shader.TileMode.CLAMP);
|
||||
|
||||
mWidth = width;
|
||||
mColor = color;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return mColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.text.Layout;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.widget.themed.ThemedTextView;
|
||||
|
||||
/**
|
||||
* An implementation of FadedTextView should fade the end of the text
|
||||
* by gecko:fadeWidth amount, if the text is too long and requires an ellipsis.
|
||||
*/
|
||||
public abstract class FadedTextView extends ThemedTextView {
|
||||
// Width of the fade effect from end of the view.
|
||||
protected final int fadeWidth;
|
||||
|
||||
public FadedTextView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setSingleLine(true);
|
||||
setEllipsize(null);
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView);
|
||||
fadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
protected int getAvailableWidth() {
|
||||
return getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
|
||||
}
|
||||
|
||||
protected boolean needsEllipsis() {
|
||||
final int width = getAvailableWidth();
|
||||
if (width <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Layout layout = getLayout();
|
||||
return (layout != null && layout.getLineWidth(0) > width);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.icons.IconCallback;
|
||||
import org.mozilla.gecko.icons.IconResponse;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Special version of ImageView for favicons.
|
||||
* Displays solid colour background around Favicon to fill space not occupied by the icon. Colour
|
||||
* selected is the dominant colour of the provided Favicon.
|
||||
*/
|
||||
public class FaviconView extends ImageView {
|
||||
private static final String LOGTAG = "GeckoFaviconView";
|
||||
|
||||
private static String DEFAULT_FAVICON_KEY = FaviconView.class.getSimpleName() + "DefaultFavicon";
|
||||
|
||||
// Default x/y-radius of the oval used to round the corners of the background (dp)
|
||||
private static final int DEFAULT_CORNER_RADIUS_DP = 4;
|
||||
|
||||
private Bitmap mIconBitmap;
|
||||
|
||||
// Reference to the unscaled bitmap, if any, to prevent repeated assignments of the same bitmap
|
||||
// to the view from causing repeated rescalings (Some of the callers do this)
|
||||
private Bitmap mUnscaledBitmap;
|
||||
|
||||
private int mActualWidth;
|
||||
private int mActualHeight;
|
||||
|
||||
// Flag indicating if the most recently assigned image is considered likely to need scaling.
|
||||
private boolean mScalingExpected;
|
||||
|
||||
// Dominant color of the favicon.
|
||||
private int mDominantColor;
|
||||
|
||||
// Paint for drawing the background.
|
||||
private static final Paint sBackgroundPaint;
|
||||
|
||||
// Size of the background rectangle.
|
||||
private final RectF mBackgroundRect;
|
||||
|
||||
// The x/y-radius of the oval used to round the corners of the background (pixels)
|
||||
private final float mBackgroundCornerRadius;
|
||||
|
||||
// Type of the border whose value is defined in attrs.xml .
|
||||
private final boolean isDominantBorderEnabled;
|
||||
|
||||
// boolean switch for overriding scaletype, whose value is defined in attrs.xml .
|
||||
private final boolean isOverrideScaleTypeEnabled;
|
||||
|
||||
// boolean switch for disabling rounded corners, value defined in attrs.xml .
|
||||
private final boolean areRoundCornersEnabled;
|
||||
|
||||
// Initializing the static paints.
|
||||
static {
|
||||
sBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
sBackgroundPaint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
public FaviconView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FaviconView, 0, 0);
|
||||
|
||||
try {
|
||||
isDominantBorderEnabled = a.getBoolean(R.styleable.FaviconView_dominantBorderEnabled, true);
|
||||
isOverrideScaleTypeEnabled = a.getBoolean(R.styleable.FaviconView_overrideScaleType, true);
|
||||
areRoundCornersEnabled = a.getBoolean(R.styleable.FaviconView_enableRoundCorners, true);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
if (isOverrideScaleTypeEnabled) {
|
||||
setScaleType(ImageView.ScaleType.CENTER);
|
||||
}
|
||||
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
|
||||
mBackgroundRect = new RectF(0, 0, 0, 0);
|
||||
mBackgroundCornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_CORNER_RADIUS_DP, metrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
// No point rechecking the image if there hasn't really been any change.
|
||||
if (w == mActualWidth && h == mActualHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
mActualWidth = w;
|
||||
mActualHeight = h;
|
||||
|
||||
mBackgroundRect.right = w;
|
||||
mBackgroundRect.bottom = h;
|
||||
|
||||
formatImage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
if (isDominantBorderEnabled) {
|
||||
sBackgroundPaint.setColor(mDominantColor & 0x7FFFFFFF);
|
||||
|
||||
if (areRoundCornersEnabled) {
|
||||
canvas.drawRoundRect(mBackgroundRect, mBackgroundCornerRadius, mBackgroundCornerRadius, sBackgroundPaint);
|
||||
} else {
|
||||
canvas.drawRect(mBackgroundRect, sBackgroundPaint);
|
||||
}
|
||||
}
|
||||
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the image for display, if the prerequisite data are available. Upscales tiny Favicons to
|
||||
* normal sized ones, replaces null bitmaps with the default Favicon, and fills all remaining space
|
||||
* in this view with the coloured background.
|
||||
*/
|
||||
private void formatImage() {
|
||||
// We're waiting for both onSizeChanged and updateImage to be called before scaling.
|
||||
if (mIconBitmap == null || mActualWidth == 0 || mActualHeight == 0) {
|
||||
showNoImage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mScalingExpected && mActualWidth != mIconBitmap.getWidth()) {
|
||||
scaleBitmap();
|
||||
// Don't scale the image every time something changes.
|
||||
mScalingExpected = false;
|
||||
}
|
||||
|
||||
setImageBitmap(mIconBitmap);
|
||||
|
||||
// After scaling, determine if we have empty space around the scaled image which we need to
|
||||
// fill with the coloured background. If applicable, show it.
|
||||
// We assume Favicons are still squares and only bother with the background if more than 3px
|
||||
// of it would be displayed.
|
||||
if (Math.abs(mIconBitmap.getWidth() - mActualWidth) < 3) {
|
||||
mDominantColor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleBitmap() {
|
||||
// If the Favicon can be resized to fill the view exactly without an enlargment of more than
|
||||
// a factor of two, do so.
|
||||
int doubledSize = mIconBitmap.getWidth() * 2;
|
||||
if (mActualWidth > doubledSize) {
|
||||
// If the view is more than twice the size of the image, just double the image size
|
||||
// and do the rest with padding.
|
||||
mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, doubledSize, doubledSize, true);
|
||||
} else {
|
||||
// Otherwise, scale the image to fill the view.
|
||||
mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, mActualWidth, mActualWidth, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon displayed in this Favicon view to the bitmap provided. If the size of the view
|
||||
* has been set, the display will be updated right away, otherwise the update will be deferred
|
||||
* until then. The key provided is used to cache the result of the calculation of the dominant
|
||||
* colour of the provided image - this value is used to draw the coloured background in this view
|
||||
* if the icon is not large enough to fill it.
|
||||
*
|
||||
* @param allowScaling If true, allows the provided bitmap to be scaled by this FaviconView.
|
||||
* Typically, you should prefer using Favicons obtained via the caching system
|
||||
* (Favicons class), so as to exploit caching.
|
||||
*/
|
||||
private void updateImageInternal(IconResponse response, boolean allowScaling) {
|
||||
// Reassigning the same bitmap? Don't bother.
|
||||
if (mUnscaledBitmap == response.getBitmap()) {
|
||||
return;
|
||||
}
|
||||
mUnscaledBitmap = response.getBitmap();
|
||||
mIconBitmap = response.getBitmap();
|
||||
mDominantColor = response.getColor();
|
||||
mScalingExpected = allowScaling;
|
||||
|
||||
// Possibly update the display.
|
||||
formatImage();
|
||||
}
|
||||
|
||||
private void showNoImage() {
|
||||
setImageDrawable(null);
|
||||
mDominantColor = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear image and background shown by this view.
|
||||
*/
|
||||
public void clearImage() {
|
||||
showNoImage();
|
||||
mUnscaledBitmap = null;
|
||||
mIconBitmap = null;
|
||||
mDominantColor = 0;
|
||||
mScalingExpected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the displayed image and apply the scaling logic.
|
||||
* The scaling logic will attempt to resize the image to fit correctly inside the view in a way
|
||||
* that avoids unreasonable levels of loss of quality.
|
||||
* Scaling is necessary only when the icon being provided is not drawn from the Favicon cache
|
||||
* introduced in Bug 914296.
|
||||
*
|
||||
* Due to Bug 913746, icons bundled for search engines are not available to the cache, so must
|
||||
* always have the scaling logic applied here. At the time of writing, this is the only case in
|
||||
* which the scaling logic here is applied.
|
||||
*/
|
||||
public void updateAndScaleImage(IconResponse response) {
|
||||
updateImageInternal(response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the image displayed in the Favicon view without scaling. Images larger than the view
|
||||
* will be centrally cropped. Images smaller than the view will be placed centrally and the
|
||||
* extra space filled with the dominant colour of the provided image.
|
||||
*/
|
||||
public void updateImage(IconResponse response) {
|
||||
updateImageInternal(response, false);
|
||||
}
|
||||
|
||||
public Bitmap getBitmap() {
|
||||
return mIconBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IconCallback implementation that will update this view after an icon has been loaded.
|
||||
*/
|
||||
public IconCallback createIconCallback() {
|
||||
return new Callback(this);
|
||||
}
|
||||
|
||||
private static class Callback implements IconCallback {
|
||||
private final WeakReference<FaviconView> viewReference;
|
||||
|
||||
private Callback(FaviconView view) {
|
||||
this.viewReference = new WeakReference<FaviconView>(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIconResponse(IconResponse response) {
|
||||
final FaviconView view = viewReference.get();
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.updateImage(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.CardView;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
|
||||
/**
|
||||
* CardView that ensures its content can fill the entire card. Use this instead of CardView
|
||||
* if you want to fill the card with e.g. images, backgrounds, etc.
|
||||
*
|
||||
* On API < 21, CardView content isn't clipped for performance reasons. We work around this by disabling
|
||||
* rounded corners on those devices.
|
||||
*/
|
||||
public class FilledCardView extends CardView {
|
||||
|
||||
public FilledCardView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
// Disable corners on < lollipop:
|
||||
// CardView only supports clipping content on API >= 21 (for performance reasons). Without
|
||||
// content clipping, any cards that provide their own content that fills the card will look
|
||||
// ugly: by default there is a 2px white edge along the top and sides (i.e. an inset corresponding
|
||||
// to the corner radius), if we disable the inset then the corners overlap.
|
||||
// It's possible to implement custom clipping, however given that the support library
|
||||
// chose not to support this for performance reasons, we too have chosen to just disable
|
||||
// corners on < 21, see Bug 1271428.
|
||||
if (AppConstants.Versions.preLollipop) {
|
||||
setRadius(0);
|
||||
}
|
||||
|
||||
setUseCompatPadding(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
public class FlowLayout extends ViewGroup {
|
||||
private int mSpacing;
|
||||
|
||||
public FlowLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public FlowLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, org.mozilla.gecko.R.styleable.FlowLayout);
|
||||
mSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_spacing, (int) context.getResources().getDimension(R.dimen.flow_layout_spacing));
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
final int childCount = getChildCount();
|
||||
int rowWidth = 0;
|
||||
int totalWidth = 0;
|
||||
int totalHeight = 0;
|
||||
boolean firstChild = true;
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
if (child.getVisibility() == GONE)
|
||||
continue;
|
||||
|
||||
measureChild(child, widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
final int childWidth = child.getMeasuredWidth();
|
||||
final int childHeight = child.getMeasuredHeight();
|
||||
|
||||
if (firstChild || (rowWidth + childWidth > parentWidth)) {
|
||||
rowWidth = 0;
|
||||
totalHeight += childHeight;
|
||||
if (!firstChild)
|
||||
totalHeight += mSpacing;
|
||||
firstChild = false;
|
||||
}
|
||||
|
||||
rowWidth += childWidth;
|
||||
|
||||
if (rowWidth > totalWidth)
|
||||
totalWidth = rowWidth;
|
||||
|
||||
rowWidth += mSpacing;
|
||||
}
|
||||
|
||||
setMeasuredDimension(totalWidth, totalHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
final int childCount = getChildCount();
|
||||
final int totalWidth = r - l;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int prevChildHeight = 0;
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
if (child.getVisibility() == GONE)
|
||||
continue;
|
||||
|
||||
final int childWidth = child.getMeasuredWidth();
|
||||
final int childHeight = child.getMeasuredHeight();
|
||||
if (x + childWidth > totalWidth) {
|
||||
x = 0;
|
||||
y += prevChildHeight + mSpacing;
|
||||
}
|
||||
prevChildHeight = childHeight;
|
||||
child.layout(x, y, x + childWidth, y + childHeight);
|
||||
x += childWidth + mSpacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.util.Base64;
|
||||
import android.view.Menu;
|
||||
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.SnackbarBuilder;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.overlays.ui.ShareDialog;
|
||||
import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
|
||||
import org.mozilla.gecko.util.IOUtils;
|
||||
import org.mozilla.gecko.util.IntentUtils;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MenuItem.OnMenuItemClickListener;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class GeckoActionProvider {
|
||||
private static final int MAX_HISTORY_SIZE_DEFAULT = 2;
|
||||
|
||||
/**
|
||||
* A listener to know when a target was selected.
|
||||
* When setting a provider, the activity can listen to this,
|
||||
* to close the menu.
|
||||
*/
|
||||
public interface OnTargetSelectedListener {
|
||||
public void onTargetSelected();
|
||||
}
|
||||
|
||||
final Context mContext;
|
||||
|
||||
public final static String DEFAULT_MIME_TYPE = "text/plain";
|
||||
|
||||
public static final String DEFAULT_HISTORY_FILE_NAME = "history.xml";
|
||||
|
||||
// History file.
|
||||
String mHistoryFileName = DEFAULT_HISTORY_FILE_NAME;
|
||||
|
||||
OnTargetSelectedListener mOnTargetListener;
|
||||
|
||||
private final Callbacks mCallbacks = new Callbacks();
|
||||
|
||||
private static final HashMap<String, GeckoActionProvider> mProviders = new HashMap<String, GeckoActionProvider>();
|
||||
|
||||
private static String getFilenameFromMimeType(String mimeType) {
|
||||
String[] mime = mimeType.split("/");
|
||||
|
||||
// All text mimetypes use the default provider
|
||||
if ("text".equals(mime[0])) {
|
||||
return DEFAULT_HISTORY_FILE_NAME;
|
||||
}
|
||||
|
||||
return "history-" + mime[0] + ".xml";
|
||||
}
|
||||
|
||||
// Gets the action provider for a particular mimetype
|
||||
public static GeckoActionProvider getForType(String mimeType, Context context) {
|
||||
if (!mProviders.keySet().contains(mimeType)) {
|
||||
GeckoActionProvider provider = new GeckoActionProvider(context);
|
||||
|
||||
// For empty types, we just return a default provider
|
||||
if (TextUtils.isEmpty(mimeType)) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
provider.setHistoryFileName(getFilenameFromMimeType(mimeType));
|
||||
mProviders.put(mimeType, provider);
|
||||
}
|
||||
return mProviders.get(mimeType);
|
||||
}
|
||||
|
||||
public GeckoActionProvider(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the action view using the default history size.
|
||||
*/
|
||||
public View onCreateActionView(final ActionViewType viewType) {
|
||||
return onCreateActionView(MAX_HISTORY_SIZE_DEFAULT, viewType);
|
||||
}
|
||||
|
||||
public View onCreateActionView(final int maxHistorySize, final ActionViewType viewType) {
|
||||
// Create the view and set its data model.
|
||||
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
|
||||
final MenuItemSwitcherLayout view;
|
||||
switch (viewType) {
|
||||
case DEFAULT:
|
||||
view = new MenuItemSwitcherLayout(mContext, null);
|
||||
break;
|
||||
|
||||
case CONTEXT_MENU:
|
||||
view = new MenuItemSwitcherLayout(mContext, null);
|
||||
view.initContextMenuStyles();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown " + ActionViewType.class.getSimpleName() + ": " + viewType);
|
||||
}
|
||||
view.addActionButtonClickListener(mCallbacks);
|
||||
|
||||
final PackageManager packageManager = mContext.getPackageManager();
|
||||
int historySize = dataModel.getDistinctActivityCountInHistory();
|
||||
if (historySize > maxHistorySize) {
|
||||
historySize = maxHistorySize;
|
||||
}
|
||||
|
||||
// Historical data is dependent on past selection of activities.
|
||||
// Activity count is determined by the number of activities that can handle
|
||||
// the particular intent. When no intent is set, the activity count is 0,
|
||||
// while the history count can be a valid number.
|
||||
if (historySize > dataModel.getActivityCount()) {
|
||||
return view;
|
||||
}
|
||||
|
||||
for (int i = 0; i < historySize; i++) {
|
||||
view.addActionButton(dataModel.getActivity(i).loadIcon(packageManager),
|
||||
dataModel.getActivity(i).loadLabel(packageManager));
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public boolean hasSubMenu() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onPrepareSubMenu(SubMenu subMenu) {
|
||||
// Clear since the order of items may change.
|
||||
subMenu.clear();
|
||||
|
||||
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
|
||||
PackageManager packageManager = mContext.getPackageManager();
|
||||
|
||||
// Populate the sub-menu with a sub set of the activities.
|
||||
final String shareDialogClassName = ShareDialog.class.getCanonicalName();
|
||||
final String sendTabLabel = mContext.getResources().getString(R.string.overlay_share_send_other);
|
||||
final int count = dataModel.getActivityCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
ResolveInfo activity = dataModel.getActivity(i);
|
||||
final CharSequence activityLabel = activity.loadLabel(packageManager);
|
||||
|
||||
// Pin internal actions to the top. Note:
|
||||
// the order here does not affect quick share.
|
||||
final int order;
|
||||
if (shareDialogClassName.equals(activity.activityInfo.name) &&
|
||||
sendTabLabel.equals(activityLabel)) {
|
||||
order = Menu.FIRST + i;
|
||||
} else {
|
||||
order = Menu.FIRST + (i | Menu.CATEGORY_SECONDARY);
|
||||
}
|
||||
|
||||
subMenu.add(0, i, order, activityLabel)
|
||||
.setIcon(activity.loadIcon(packageManager))
|
||||
.setOnMenuItemClickListener(mCallbacks);
|
||||
}
|
||||
}
|
||||
|
||||
public void setHistoryFileName(String historyFile) {
|
||||
mHistoryFileName = historyFile;
|
||||
}
|
||||
|
||||
public Intent getIntent() {
|
||||
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
|
||||
return dataModel.getIntent();
|
||||
}
|
||||
|
||||
public void setIntent(Intent intent) {
|
||||
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
|
||||
dataModel.setIntent(intent);
|
||||
|
||||
// Inform the target listener to refresh it's UI, if needed.
|
||||
if (mOnTargetListener != null) {
|
||||
mOnTargetListener.onTargetSelected();
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnTargetSelectedListener(OnTargetSelectedListener listener) {
|
||||
mOnTargetListener = listener;
|
||||
}
|
||||
|
||||
public ArrayList<ResolveInfo> getSortedActivities() {
|
||||
ArrayList<ResolveInfo> infos = new ArrayList<ResolveInfo>();
|
||||
|
||||
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
|
||||
|
||||
// Populate the sub-menu with a sub set of the activities.
|
||||
final int count = dataModel.getActivityCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
infos.add(dataModel.getActivity(i));
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
public void chooseActivity(int position) {
|
||||
mCallbacks.chooseActivity(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for handling default activity / menu item clicks.
|
||||
*/
|
||||
private class Callbacks implements OnMenuItemClickListener,
|
||||
OnClickListener {
|
||||
void chooseActivity(int index) {
|
||||
final ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
|
||||
final Intent launchIntent = dataModel.chooseActivity(index);
|
||||
if (launchIntent != null) {
|
||||
// This may cause a download to happen. Make sure we're on the background thread.
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Share image downloads the image before sharing it.
|
||||
String type = launchIntent.getType();
|
||||
if (Intent.ACTION_SEND.equals(launchIntent.getAction()) && type != null && type.startsWith("image/")) {
|
||||
downloadImageForIntent(launchIntent);
|
||||
}
|
||||
|
||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
|
||||
mContext.startActivity(launchIntent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mOnTargetListener != null) {
|
||||
mOnTargetListener.onTargetSelected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
chooseActivity(item.getItemId());
|
||||
|
||||
// Context: Sharing via chrome mainmenu list (no explicit session is active)
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "actionprovider");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Integer index = (Integer) view.getTag();
|
||||
chooseActivity(index);
|
||||
|
||||
// Context: Sharing via chrome mainmenu and content contextmenu quickshare (no explicit session is active)
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.BUTTON, "actionprovider");
|
||||
}
|
||||
}
|
||||
|
||||
public enum ActionViewType {
|
||||
DEFAULT,
|
||||
CONTEXT_MENU,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Downloads the URI pointed to by a share intent, and alters the intent to point to the
|
||||
* locally stored file.
|
||||
*
|
||||
* @param intent share intent to alter in place.
|
||||
*/
|
||||
public void downloadImageForIntent(final Intent intent) {
|
||||
final String src = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT);
|
||||
final File dir = GeckoApp.getTempDirectory();
|
||||
|
||||
if (src == null || dir == null) {
|
||||
// We should be, but currently aren't, statically guaranteed an Activity context.
|
||||
// Try our best.
|
||||
if (mContext instanceof Activity) {
|
||||
SnackbarBuilder.builder((Activity) mContext)
|
||||
.message(mContext.getApplicationContext().getString(R.string.share_image_failed))
|
||||
.duration(Snackbar.LENGTH_LONG)
|
||||
.buildAndShow();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GeckoApp.deleteTempFiles();
|
||||
|
||||
String type = intent.getType();
|
||||
OutputStream os = null;
|
||||
try {
|
||||
// Create a temporary file for the image
|
||||
if (src.startsWith("data:")) {
|
||||
final int dataStart = src.indexOf(",");
|
||||
|
||||
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
|
||||
|
||||
// If we weren't given an explicit mimetype, try to dig one out of the data uri.
|
||||
if (TextUtils.isEmpty(extension) && dataStart > 5) {
|
||||
type = src.substring(5, dataStart).replace(";base64", "");
|
||||
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
|
||||
}
|
||||
|
||||
final File imageFile = File.createTempFile("image", "." + extension, dir);
|
||||
os = new FileOutputStream(imageFile);
|
||||
|
||||
byte[] buf = Base64.decode(src.substring(dataStart + 1), Base64.DEFAULT);
|
||||
os.write(buf);
|
||||
|
||||
// Only alter the intent when we're sure everything has worked
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
|
||||
} else {
|
||||
InputStream is = null;
|
||||
try {
|
||||
final byte[] buf = new byte[2048];
|
||||
final URL url = new URL(src);
|
||||
final String filename = URLUtil.guessFileName(src, null, type);
|
||||
is = url.openStream();
|
||||
|
||||
final File imageFile = new File(dir, filename);
|
||||
os = new FileOutputStream(imageFile);
|
||||
|
||||
int length;
|
||||
while ((length = is.read(buf)) != -1) {
|
||||
os.write(buf, 0, length);
|
||||
}
|
||||
|
||||
// Only alter the intent when we're sure everything has worked
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
|
||||
} finally {
|
||||
IOUtils.safeStreamClose(is);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
// If something went wrong, we'll just leave the intent un-changed
|
||||
} finally {
|
||||
IOUtils.safeStreamClose(os);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.menu.GeckoMenu;
|
||||
import org.mozilla.gecko.menu.GeckoMenuInflater;
|
||||
import org.mozilla.gecko.menu.MenuPanel;
|
||||
import org.mozilla.gecko.menu.MenuPopup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A PopupMenu that uses the custom GeckoMenu. This menu is
|
||||
* usually tied to an anchor, and show as a dropdrown from the anchor.
|
||||
*/
|
||||
public class GeckoPopupMenu implements GeckoMenu.Callback,
|
||||
GeckoMenu.MenuPresenter {
|
||||
|
||||
// An interface for listeners for dismissal.
|
||||
public static interface OnDismissListener {
|
||||
public boolean onDismiss(GeckoMenu menu);
|
||||
}
|
||||
|
||||
// An interface for listeners for menu item click events.
|
||||
public static interface OnMenuItemClickListener {
|
||||
public boolean onMenuItemClick(MenuItem item);
|
||||
}
|
||||
|
||||
// An interface for listeners for menu item long click events.
|
||||
public static interface OnMenuItemLongClickListener {
|
||||
public boolean onMenuItemLongClick(MenuItem item);
|
||||
}
|
||||
|
||||
private View mAnchor;
|
||||
|
||||
private MenuPopup mMenuPopup;
|
||||
private MenuPanel mMenuPanel;
|
||||
|
||||
private GeckoMenu mMenu;
|
||||
private GeckoMenuInflater mMenuInflater;
|
||||
|
||||
private OnDismissListener mDismissListener;
|
||||
private OnMenuItemClickListener mClickListener;
|
||||
private OnMenuItemLongClickListener mLongClickListener;
|
||||
|
||||
public GeckoPopupMenu(Context context) {
|
||||
initialize(context, null);
|
||||
}
|
||||
|
||||
public GeckoPopupMenu(Context context, View anchor) {
|
||||
initialize(context, anchor);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates an empty menu and attaches the necessary listeners.
|
||||
* If an anchor is supplied, it is stored as well.
|
||||
*/
|
||||
private void initialize(Context context, View anchor) {
|
||||
mMenu = new GeckoMenu(context, null);
|
||||
mMenu.setCallback(this);
|
||||
mMenu.setMenuPresenter(this);
|
||||
mMenuInflater = new GeckoMenuInflater(context);
|
||||
|
||||
mMenuPopup = new MenuPopup(context);
|
||||
mMenuPanel = new MenuPanel(context, null);
|
||||
|
||||
mMenuPanel.addView(mMenu);
|
||||
mMenuPopup.setPanelView(mMenuPanel);
|
||||
|
||||
setAnchor(anchor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the menu that is current being shown.
|
||||
*
|
||||
* @return The menu being shown.
|
||||
*/
|
||||
public GeckoMenu getMenu() {
|
||||
return mMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the menu inflater that was used to create the menu.
|
||||
*
|
||||
* @return The menu inflater used.
|
||||
*/
|
||||
public MenuInflater getMenuInflater() {
|
||||
return mMenuInflater;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates a menu resource to the menu using the menu inflater.
|
||||
*
|
||||
* @param menuRes The menu resource to be inflated.
|
||||
*/
|
||||
public void inflate(int menuRes) {
|
||||
if (menuRes > 0) {
|
||||
mMenuInflater.inflate(menuRes, mMenu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a different anchor after the menu is inflated.
|
||||
*
|
||||
* @param anchor The new anchor for the popup.
|
||||
*/
|
||||
public void setAnchor(View anchor) {
|
||||
mAnchor = anchor;
|
||||
|
||||
// Reposition the popup if the anchor changes while it's showing.
|
||||
if (mMenuPopup.isShowing()) {
|
||||
mMenuPopup.dismiss();
|
||||
mMenuPopup.showAsDropDown(mAnchor);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnDismissListener(OnDismissListener listener) {
|
||||
mDismissListener = listener;
|
||||
}
|
||||
|
||||
public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
|
||||
mClickListener = listener;
|
||||
}
|
||||
|
||||
public void setOnMenuItemLongClickListener(OnMenuItemLongClickListener listener) {
|
||||
mLongClickListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the inflated menu.
|
||||
*/
|
||||
public void show() {
|
||||
if (!mMenuPopup.isShowing())
|
||||
mMenuPopup.showAsDropDown(mAnchor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the inflated menu.
|
||||
*/
|
||||
public void dismiss() {
|
||||
if (mMenuPopup.isShowing()) {
|
||||
mMenuPopup.dismiss();
|
||||
|
||||
if (mDismissListener != null)
|
||||
mDismissListener.onDismiss(mMenu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (mClickListener != null) {
|
||||
return mClickListener.onMenuItemClick(item);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemLongClick(MenuItem item) {
|
||||
if (mLongClickListener != null) {
|
||||
return mLongClickListener.onMenuItemLongClick(item);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openMenu() {
|
||||
show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMenu(View menu) {
|
||||
mMenuPanel.removeAllViews();
|
||||
mMenuPanel.addView(menu);
|
||||
|
||||
openMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeMenu() {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.home.CombinedHistoryItem;
|
||||
|
||||
public class HistoryDividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int mDividerHeight;
|
||||
private final Paint mDividerPaint;
|
||||
|
||||
public HistoryDividerItemDecoration(Context context) {
|
||||
mDividerHeight = (int) context.getResources().getDimension(R.dimen.page_row_divider_height);
|
||||
|
||||
mDividerPaint = new Paint();
|
||||
mDividerPaint.setColor(ContextCompat.getColor(context, R.color.toolbar_divider_grey));
|
||||
mDividerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
final int position = parent.getChildAdapterPosition(view);
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
// This view is no longer corresponds to an adapter position (pending changes).
|
||||
return;
|
||||
}
|
||||
|
||||
if (parent.getAdapter().getItemViewType(position) !=
|
||||
CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) {
|
||||
outRect.set(0, 0, 0, mDividerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
|
||||
if (parent.getChildCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < parent.getChildCount(); i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
final int position = parent.getChildAdapterPosition(child);
|
||||
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
// This view is no longer corresponds to an adapter position (pending changes).
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parent.getAdapter().getItemViewType(position) !=
|
||||
CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) {
|
||||
final float bottom = child.getBottom() + child.getTranslationY();
|
||||
c.drawRect(0, bottom, parent.getWidth(), bottom + mDividerHeight, mDividerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TabWidget;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class IconTabWidget extends TabWidget {
|
||||
OnTabChangedListener mListener;
|
||||
private final int mButtonLayoutId;
|
||||
private final boolean mIsIcon;
|
||||
|
||||
public static interface OnTabChangedListener {
|
||||
public void onTabChanged(int tabIndex);
|
||||
}
|
||||
|
||||
public IconTabWidget(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IconTabWidget);
|
||||
mButtonLayoutId = a.getResourceId(R.styleable.IconTabWidget_android_layout, 0);
|
||||
mIsIcon = (a.getInt(R.styleable.IconTabWidget_display, 0x00) == 0x00);
|
||||
a.recycle();
|
||||
|
||||
if (mButtonLayoutId == 0) {
|
||||
throw new RuntimeException("You must supply layout attribute");
|
||||
}
|
||||
}
|
||||
|
||||
public View addTab(final int imageResId, final int stringResId) {
|
||||
View button = LayoutInflater.from(getContext()).inflate(mButtonLayoutId, this, false);
|
||||
if (mIsIcon) {
|
||||
((ImageButton) button).setImageResource(imageResId);
|
||||
button.setContentDescription(getContext().getString(stringResId));
|
||||
} else {
|
||||
((TextView) button).setText(getContext().getString(stringResId));
|
||||
}
|
||||
|
||||
addView(button);
|
||||
button.setOnClickListener(new TabClickListener(getTabCount() - 1));
|
||||
button.setOnFocusChangeListener(this);
|
||||
return button;
|
||||
}
|
||||
|
||||
public void setTabSelectionListener(OnTabChangedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View view, boolean hasFocus) {
|
||||
}
|
||||
|
||||
private class TabClickListener implements OnClickListener {
|
||||
private final int mIndex;
|
||||
|
||||
public TabClickListener(int index) {
|
||||
mIndex = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (mListener != null)
|
||||
mListener.onTabChanged(mIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the Drawable icon corresponding to the given panel.
|
||||
* @param panel to fetch icon for.
|
||||
* @return Drawable instance, or null if no icon is being displayed, or the icon does not exist.
|
||||
*/
|
||||
public Drawable getIconDrawable(int index) {
|
||||
if (!mIsIcon) {
|
||||
return null;
|
||||
}
|
||||
// We can have multiple views in the tabs panel for each child. This finds the
|
||||
// first view corresponding to the given tab. This varies by Android
|
||||
// version. The first view should always be our ImageButton, but let's
|
||||
// be safe.
|
||||
final View view = getChildTabViewAt(index);
|
||||
if (view instanceof ImageButton) {
|
||||
return ((ImageButton) view).getDrawable();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setIconDrawable(int index, int resource) {
|
||||
if (!mIsIcon) {
|
||||
return;
|
||||
}
|
||||
// We can have multiple views in the tabs panel for each child. This finds the
|
||||
// first view corresponding to the given tab. This varies by Android
|
||||
// version. The first view should always be our ImageButton, but let's
|
||||
// be safe.
|
||||
final View view = getChildTabViewAt(index);
|
||||
if (view instanceof ImageButton) {
|
||||
((ImageButton) view).setImageResource(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class LoginDoorHanger extends DoorHanger {
|
||||
private static final String LOGTAG = "LoginDoorHanger";
|
||||
private enum ActionType { EDIT, SELECT }
|
||||
|
||||
private final TextView mMessage;
|
||||
private final DoorhangerConfig.ButtonConfig mButtonConfig;
|
||||
|
||||
public LoginDoorHanger(Context context, DoorhangerConfig config) {
|
||||
super(context, config, Type.LOGIN);
|
||||
|
||||
mMessage = (TextView) findViewById(R.id.doorhanger_message);
|
||||
mIcon.setImageResource(R.drawable.icon_key);
|
||||
mIcon.setVisibility(View.VISIBLE);
|
||||
|
||||
mButtonConfig = config.getPositiveButtonConfig();
|
||||
|
||||
loadConfig(config);
|
||||
}
|
||||
|
||||
private void setMessage(String message) {
|
||||
Spanned markupMessage = Html.fromHtml(message);
|
||||
mMessage.setText(markupMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadConfig(DoorhangerConfig config) {
|
||||
setOptions(config.getOptions());
|
||||
setMessage(config.getMessage());
|
||||
// Store the positive callback id for nested dialogs that need the same callback id.
|
||||
addButtonsToLayout(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getContentResource() {
|
||||
return R.layout.login_doorhanger;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setOptions(final JSONObject options) {
|
||||
super.setOptions(options);
|
||||
|
||||
final JSONObject actionText = options.optJSONObject("actionText");
|
||||
addActionText(actionText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
|
||||
return new Button.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
|
||||
|
||||
final JSONObject response = new JSONObject();
|
||||
try {
|
||||
response.put("callback", id);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error making doorhanger response message", e);
|
||||
}
|
||||
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sub-text to the doorhanger and add the click action.
|
||||
*
|
||||
* If the parsing the action from the JSON throws, the text is left visible, but there is no
|
||||
* click action.
|
||||
* @param actionTextObj JSONObject containing blob for making an action.
|
||||
*/
|
||||
private void addActionText(JSONObject actionTextObj) {
|
||||
if (actionTextObj == null) {
|
||||
mLink.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make action.
|
||||
try {
|
||||
final JSONObject bundle = actionTextObj.getJSONObject("bundle");
|
||||
final ActionType type = ActionType.valueOf(actionTextObj.getString("type"));
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
|
||||
|
||||
switch (type) {
|
||||
case EDIT:
|
||||
builder.setTitle(mResources.getString(R.string.doorhanger_login_edit_title));
|
||||
|
||||
final View view = LayoutInflater.from(mContext).inflate(R.layout.login_edit_dialog, null);
|
||||
final EditText username = (EditText) view.findViewById(R.id.username_edit);
|
||||
username.setText(bundle.getString("username"));
|
||||
final EditText password = (EditText) view.findViewById(R.id.password_edit);
|
||||
password.setText(bundle.getString("password"));
|
||||
final CheckBox passwordCheckbox = (CheckBox) view.findViewById(R.id.checkbox_toggle_password);
|
||||
passwordCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (isChecked) {
|
||||
password.setTransformationMethod(null);
|
||||
} else {
|
||||
password.setTransformationMethod(PasswordTransformationMethod.getInstance());
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setView(view);
|
||||
|
||||
builder.setPositiveButton(mButtonConfig.label, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
response.put("callback", mButtonConfig.callback);
|
||||
final JSONObject inputs = new JSONObject();
|
||||
inputs.put("username", username.getText());
|
||||
inputs.put("password", password.getText());
|
||||
response.put("inputs", inputs);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error creating doorhanger reply message");
|
||||
response = null;
|
||||
Toast.makeText(mContext, mResources.getString(R.string.doorhanger_login_edit_toast_error), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
String text = actionTextObj.optString("text");
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
text = mResources.getString(R.string.doorhanger_login_no_username);
|
||||
}
|
||||
mLink.setText(text);
|
||||
mLink.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case SELECT:
|
||||
try {
|
||||
builder.setTitle(mResources.getString(R.string.doorhanger_login_select_title));
|
||||
final JSONArray logins = bundle.getJSONArray("logins");
|
||||
final int numLogins = logins.length();
|
||||
final CharSequence[] usernames = new CharSequence[numLogins];
|
||||
final String[] passwords = new String[numLogins];
|
||||
final String noUser = mResources.getString(R.string.doorhanger_login_no_username);
|
||||
for (int i = 0; i < numLogins; i++) {
|
||||
final JSONObject login = (JSONObject) logins.get(i);
|
||||
String user = login.getString("username");
|
||||
if (TextUtils.isEmpty(user)) {
|
||||
user = noUser;
|
||||
}
|
||||
usernames[i] = user;
|
||||
passwords[i] = login.getString("password");
|
||||
}
|
||||
builder.setItems(usernames, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final JSONObject response = new JSONObject();
|
||||
try {
|
||||
response.put("callback", mButtonConfig.callback);
|
||||
response.put("password", passwords[which]);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error making login select dialog JSON", e);
|
||||
}
|
||||
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
mLink.setText(R.string.doorhanger_login_select_action_text);
|
||||
mLink.setVisibility(View.VISIBLE);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Problem creating list of logins");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
final Dialog dialog = builder.create();
|
||||
mLink.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error fetching actionText from JSON", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
/**
|
||||
* {@link RecyclerViewClickSupport} implementation that will notify an OnClickListener about clicks and long clicks
|
||||
* on items displayed by the RecyclerView.
|
||||
* @see <a href="http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/">littlerobots.nl</a>
|
||||
*/
|
||||
public class RecyclerViewClickSupport {
|
||||
private final RecyclerView mRecyclerView;
|
||||
private OnItemClickListener mOnItemClickListener;
|
||||
private OnItemLongClickListener mOnItemLongClickListener;
|
||||
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnItemClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
}
|
||||
};
|
||||
private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (mOnItemLongClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
private RecyclerView.OnChildAttachStateChangeListener mAttachListener
|
||||
= new RecyclerView.OnChildAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(View view) {
|
||||
if (mOnItemClickListener != null) {
|
||||
view.setOnClickListener(mOnClickListener);
|
||||
}
|
||||
if (mOnItemLongClickListener != null) {
|
||||
view.setOnLongClickListener(mOnLongClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(View view) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private RecyclerViewClickSupport(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
mRecyclerView.setTag(R.id.recycler_view_click_support, this);
|
||||
mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
|
||||
}
|
||||
|
||||
public static RecyclerViewClickSupport addTo(RecyclerView view) {
|
||||
RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
|
||||
if (support == null) {
|
||||
support = new RecyclerViewClickSupport(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public static RecyclerViewClickSupport removeFrom(RecyclerView view) {
|
||||
RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
|
||||
if (support != null) {
|
||||
support.detach(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public RecyclerViewClickSupport setOnItemClickListener(OnItemClickListener listener) {
|
||||
mOnItemClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RecyclerViewClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
|
||||
mOnItemLongClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void detach(RecyclerView view) {
|
||||
view.removeOnChildAttachStateChangeListener(mAttachListener);
|
||||
view.setTag(R.id.recycler_view_click_support, null);
|
||||
}
|
||||
|
||||
public interface OnItemClickListener {
|
||||
|
||||
void onItemClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
|
||||
public interface OnItemLongClickListener {
|
||||
|
||||
boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.Shape;
|
||||
|
||||
public class ResizablePathDrawable extends ShapeDrawable {
|
||||
// An attribute mirroring the super class' value. getAlpha() is only
|
||||
// available in API 19+ so to use that alpha value, we have to mirror it.
|
||||
private int alpha = 255;
|
||||
|
||||
private final ColorStateList colorStateList;
|
||||
private int currentColor;
|
||||
|
||||
public ResizablePathDrawable(NonScaledPathShape shape, int color) {
|
||||
this(shape, ColorStateList.valueOf(color));
|
||||
}
|
||||
|
||||
public ResizablePathDrawable(NonScaledPathShape shape, ColorStateList colorStateList) {
|
||||
super(shape);
|
||||
this.colorStateList = colorStateList;
|
||||
updateColor(getState());
|
||||
}
|
||||
|
||||
private boolean updateColor(int[] stateSet) {
|
||||
int newColor = colorStateList.getColorForState(stateSet, Color.WHITE);
|
||||
if (newColor != currentColor) {
|
||||
currentColor = newColor;
|
||||
alpha = Color.alpha(currentColor);
|
||||
invalidateSelf();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
final NonScaledPathShape shape = (NonScaledPathShape) getShape();
|
||||
return shape.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateful() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
|
||||
paint.setColor(currentColor);
|
||||
// setAlpha overrides the alpha value in set color. Since we just set the color,
|
||||
// the alpha value is reset: override the alpha value with the old value. We don't
|
||||
// set alpha if the color is transparent.
|
||||
//
|
||||
// Note: We *should* be able to call Shape.setAlpha, rather than Paint.setAlpha, but
|
||||
// then the opacity doesn't change - dunno why but probably not worth the time.
|
||||
if (currentColor != Color.TRANSPARENT) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
super.onDraw(shape, canvas, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(final int alpha) {
|
||||
super.setAlpha(alpha);
|
||||
this.alpha = alpha;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] stateSet) {
|
||||
return updateColor(stateSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Path-based shape implementation that re-creates the path
|
||||
* when it gets resized as opposed to PathShape's scaling
|
||||
* behaviour.
|
||||
*/
|
||||
public static class NonScaledPathShape extends Shape {
|
||||
private Path path;
|
||||
|
||||
public NonScaledPathShape() {
|
||||
path = new Path();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas, Paint paint) {
|
||||
// No point in drawing the shape if it's not
|
||||
// going to be visible.
|
||||
if (paint.getColor() == Color.TRANSPARENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
protected Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NonScaledPathShape clone() throws CloneNotSupportedException {
|
||||
final NonScaledPathShape clonedShape = (NonScaledPathShape) super.clone();
|
||||
clonedShape.path = new Path(path);
|
||||
return clonedShape;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class RoundedCornerLayout extends LinearLayout {
|
||||
private static final String LOGTAG = "Gecko" + RoundedCornerLayout.class.getSimpleName();
|
||||
private float cornerRadius;
|
||||
|
||||
private Path path;
|
||||
boolean cannotClipPath;
|
||||
|
||||
public RoundedCornerLayout(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public RoundedCornerLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public RoundedCornerLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
// Bug 1201081 - clipPath with hardware acceleration crashes on r11-18.
|
||||
cannotClipPath = !AppConstants.Versions.feature19Plus;
|
||||
|
||||
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
|
||||
cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
|
||||
getResources().getDimensionPixelSize(R.dimen.doorhanger_rounded_corner_radius), metrics);
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
if (cannotClipPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
final RectF r = new RectF(0, 0, w, h);
|
||||
path = new Path();
|
||||
path.addRoundRect(r, cornerRadius, cornerRadius, Path.Direction.CW);
|
||||
path.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (cannotClipPath) {
|
||||
super.draw(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.clipPath(path);
|
||||
super.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class SiteLogins {
|
||||
private final JSONArray logins;
|
||||
|
||||
public SiteLogins(JSONArray logins) {
|
||||
this.logins = logins;
|
||||
}
|
||||
|
||||
public JSONArray getLogins() {
|
||||
return logins;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
final class SquaredImageView extends ImageView {
|
||||
public SquaredImageView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public SquaredImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
public class SquaredRelativeLayout extends RelativeLayout {
|
||||
public SquaredRelativeLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public SquaredRelativeLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SquaredRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int squareMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
|
||||
|
||||
super.onMeasure(squareMeasureSpec, squareMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
/*
|
||||
* Copyright 2012 Roman Nurik
|
||||
*
|
||||
* 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 org.mozilla.gecko.widget;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AbsListView.RecyclerListener;
|
||||
import android.widget.ListView;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
/**
|
||||
* This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA)
|
||||
* of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async
|
||||
* adapters.
|
||||
*
|
||||
* A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView}
|
||||
* dismissable. {@link ListView} is given special treatment because by default it handles touches
|
||||
* for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
|
||||
* handling list item clicks, etc.
|
||||
*
|
||||
* <p>After creating the listener, the caller should also call
|
||||
* {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
|
||||
* in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
|
||||
* already assigned, the caller should still pass scroll changes through to this listener. This will
|
||||
* ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
|
||||
* scrolling.</p>
|
||||
*
|
||||
* <p>Example usage:</p>
|
||||
*
|
||||
* <pre>
|
||||
* SwipeDismissListViewTouchListener touchListener =
|
||||
* new SwipeDismissListViewTouchListener(
|
||||
* listView,
|
||||
* new SwipeDismissListViewTouchListener.OnDismissCallback() {
|
||||
* public void onDismiss(ListView listView, int[] reverseSortedPositions) {
|
||||
* for (int position : reverseSortedPositions) {
|
||||
* adapter.remove(adapter.getItem(position));
|
||||
* }
|
||||
* adapter.notifyDataSetChanged();
|
||||
* }
|
||||
* });
|
||||
* listView.setOnTouchListener(touchListener);
|
||||
* listView.setOnScrollListener(touchListener.makeScrollListener());
|
||||
* </pre>
|
||||
*
|
||||
* <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
|
||||
* see {@link SwipeDismissTouchListener}.</p>
|
||||
*
|
||||
* @see SwipeDismissTouchListener
|
||||
*/
|
||||
public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
|
||||
// Cached ViewConfiguration and system-wide constant values
|
||||
private final int mSlop;
|
||||
private final int mMinFlingVelocity;
|
||||
private final int mMaxFlingVelocity;
|
||||
private final long mAnimationTime;
|
||||
|
||||
// Fixed properties
|
||||
private final ListView mListView;
|
||||
private final OnDismissCallback mCallback;
|
||||
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
|
||||
|
||||
// Transient properties
|
||||
private float mDownX;
|
||||
private boolean mSwiping;
|
||||
private VelocityTracker mVelocityTracker;
|
||||
private int mDownPosition;
|
||||
private View mDownView;
|
||||
private boolean mPaused;
|
||||
private boolean mDismissing;
|
||||
|
||||
/**
|
||||
* The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
|
||||
* about a successful dismissal of a list item.
|
||||
*/
|
||||
public interface OnDismissCallback {
|
||||
/**
|
||||
* Called when the user has indicated they she would like to dismiss one or more list item
|
||||
* positions.
|
||||
*
|
||||
* @param listView The originating {@link ListView}.
|
||||
* @param position The position being dismissed.
|
||||
*/
|
||||
void onDismiss(ListView listView, int position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new swipe-to-dismiss touch listener for the given list view.
|
||||
*
|
||||
* @param listView The list view whose items should be dismissable.
|
||||
* @param callback The callback to trigger when the user has indicated that she would like to
|
||||
* dismiss one or more list items.
|
||||
*/
|
||||
public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
|
||||
ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
|
||||
mSlop = vc.getScaledTouchSlop();
|
||||
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
|
||||
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
|
||||
mAnimationTime = listView.getContext().getResources().getInteger(
|
||||
android.R.integer.config_shortAnimTime);
|
||||
mListView = listView;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
|
||||
*
|
||||
* @param enabled Whether or not to watch for gestures.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
mPaused = !enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
|
||||
* {@link ListView} using
|
||||
* {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
|
||||
* If a scroll listener is already assigned, the caller should still pass scroll changes
|
||||
* through to this listener. This will ensure that this
|
||||
* {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
|
||||
*
|
||||
* @see {@link SwipeDismissListViewTouchListener}
|
||||
*/
|
||||
public AbsListView.OnScrollListener makeScrollListener() {
|
||||
return new AbsListView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
|
||||
setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the
|
||||
* {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}.
|
||||
*/
|
||||
public AbsListView.RecyclerListener makeRecyclerListener() {
|
||||
return new AbsListView.RecyclerListener() {
|
||||
@Override
|
||||
public void onMovedToScrapHeap(View view) {
|
||||
final Object tag = view.getTag(R.id.original_height);
|
||||
|
||||
// To reset the view to the correct height after its animation, the view's height
|
||||
// is stored in its tag. Reset the view here.
|
||||
if (tag instanceof Integer) {
|
||||
view.setAlpha(1f);
|
||||
view.setTranslationX(0);
|
||||
final ViewGroup.LayoutParams lp = view.getLayoutParams();
|
||||
lp.height = (int) tag;
|
||||
view.setLayoutParams(lp);
|
||||
view.setTag(R.id.original_height, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
if (mViewWidth < 2) {
|
||||
mViewWidth = mListView.getWidth();
|
||||
}
|
||||
|
||||
switch (motionEvent.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
if (mPaused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mDismissing) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: ensure this is a finger, and set a flag
|
||||
|
||||
// Find the child view that was touched (perform a hit test)
|
||||
Rect rect = new Rect();
|
||||
int childCount = mListView.getChildCount();
|
||||
int[] listViewCoords = new int[2];
|
||||
mListView.getLocationOnScreen(listViewCoords);
|
||||
int x = (int) motionEvent.getRawX() - listViewCoords[0];
|
||||
int y = (int) motionEvent.getRawY() - listViewCoords[1];
|
||||
View child;
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
child = mListView.getChildAt(i);
|
||||
child.getHitRect(rect);
|
||||
if (rect.contains(x, y)) {
|
||||
mDownView = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mDownView != null) {
|
||||
mDownX = motionEvent.getRawX();
|
||||
mDownPosition = mListView.getPositionForView(mDownView);
|
||||
|
||||
mVelocityTracker = VelocityTracker.obtain();
|
||||
mVelocityTracker.addMovement(motionEvent);
|
||||
}
|
||||
view.onTouchEvent(motionEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (mVelocityTracker == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
float deltaX = motionEvent.getRawX() - mDownX;
|
||||
mVelocityTracker.addMovement(motionEvent);
|
||||
mVelocityTracker.computeCurrentVelocity(1000);
|
||||
float velocityX = Math.abs(mVelocityTracker.getXVelocity());
|
||||
float velocityY = Math.abs(mVelocityTracker.getYVelocity());
|
||||
boolean dismiss = false;
|
||||
boolean dismissRight = false;
|
||||
if (Math.abs(deltaX) > mViewWidth / 2) {
|
||||
dismiss = true;
|
||||
dismissRight = deltaX > 0;
|
||||
} else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
|
||||
&& velocityY < velocityX) {
|
||||
dismiss = true;
|
||||
dismissRight = mVelocityTracker.getXVelocity() > 0;
|
||||
}
|
||||
if (dismiss) {
|
||||
// dismiss
|
||||
mDismissing = true;
|
||||
final View downView = mDownView; // mDownView gets null'd before animation ends
|
||||
final int downPosition = mDownPosition;
|
||||
mDownView.animate()
|
||||
.translationX(dismissRight ? mViewWidth : -mViewWidth)
|
||||
.alpha(0)
|
||||
.setDuration(mAnimationTime)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
performDismiss(downView, downPosition);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// cancel
|
||||
mDownView.animate()
|
||||
.translationX(0)
|
||||
.alpha(1)
|
||||
.setDuration(mAnimationTime)
|
||||
.setListener(null);
|
||||
}
|
||||
|
||||
if (mVelocityTracker != null) {
|
||||
mVelocityTracker.recycle();
|
||||
mVelocityTracker = null;
|
||||
}
|
||||
|
||||
mDownX = 0;
|
||||
mDownView = null;
|
||||
mDownPosition = ListView.INVALID_POSITION;
|
||||
mSwiping = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (mVelocityTracker == null || mPaused) {
|
||||
break;
|
||||
}
|
||||
|
||||
mVelocityTracker.addMovement(motionEvent);
|
||||
float deltaX = motionEvent.getRawX() - mDownX;
|
||||
if (Math.abs(deltaX) > mSlop) {
|
||||
mSwiping = true;
|
||||
mListView.requestDisallowInterceptTouchEvent(true);
|
||||
|
||||
// Cancel ListView's touch (un-highlighting the item)
|
||||
MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
|
||||
cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
|
||||
(motionEvent.getActionIndex()
|
||||
<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
|
||||
mListView.onTouchEvent(cancelEvent);
|
||||
cancelEvent.recycle();
|
||||
}
|
||||
|
||||
if (mSwiping) {
|
||||
mDownView.setTranslationX(deltaX);
|
||||
mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes.
|
||||
*
|
||||
* @param dismissView ListView item to dismiss
|
||||
* @param dismissPosition Position of dismissed item
|
||||
*/
|
||||
private void performDismiss(final View dismissView, final int dismissPosition) {
|
||||
final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
|
||||
final int originalHeight = lp.height;
|
||||
|
||||
ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime);
|
||||
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
// Since the view is still a part of the ListView, we can't reset the animated
|
||||
// properties yet; otherwise, the view would briefly reappear. Store the original
|
||||
// height in the view's tag to flag it for the recycler. This is racy since the user
|
||||
// could scroll the dismissed view off the screen, then back on the screen, before
|
||||
// it's removed from the adapter, causing the dismissed view to briefly reappear.
|
||||
dismissView.setTag(R.id.original_height, originalHeight);
|
||||
|
||||
mCallback.onDismiss(mListView, dismissPosition);
|
||||
mDismissing = false;
|
||||
}
|
||||
});
|
||||
|
||||
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
lp.height = (Integer) valueAnimator.getAnimatedValue();
|
||||
dismissView.setLayoutParams(lp);
|
||||
}
|
||||
});
|
||||
|
||||
animator.start();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
|
||||
|
||||
|
||||
public class TabThumbnailWrapper extends ThemedRelativeLayout {
|
||||
private boolean mRecording;
|
||||
private static final int[] STATE_RECORDING = { R.attr.state_recording };
|
||||
|
||||
public TabThumbnailWrapper(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public TabThumbnailWrapper(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (mRecording) {
|
||||
mergeDrawableStates(drawableState, STATE_RECORDING);
|
||||
}
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
public void setRecording(boolean recording) {
|
||||
if (mRecording != recording) {
|
||||
mRecording = recording;
|
||||
refreshDrawableState();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.widget.themed.ThemedImageView;
|
||||
|
||||
/* Special version of ImageView for thumbnails. Scales a thumbnail so that it maintains its aspect
|
||||
* ratio and so that the images width and height are the same size or greater than the view size
|
||||
*/
|
||||
public class ThumbnailView extends ThemedImageView {
|
||||
private static final String LOGTAG = "GeckoThumbnailView";
|
||||
|
||||
final private Matrix mMatrix;
|
||||
private int mWidthSpec = -1;
|
||||
private int mHeightSpec = -1;
|
||||
private boolean mLayoutChanged;
|
||||
private boolean mScale = false;
|
||||
|
||||
public ThumbnailView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mMatrix = new Matrix();
|
||||
mLayoutChanged = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
if (!mScale) {
|
||||
super.onDraw(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable d = getDrawable();
|
||||
if (mLayoutChanged) {
|
||||
int w1 = d.getIntrinsicWidth();
|
||||
int h1 = d.getIntrinsicHeight();
|
||||
int w2 = getWidth();
|
||||
int h2 = getHeight();
|
||||
|
||||
float scale = ((w2 / h2) < (w1 / h1)) ? (float) h2 / h1 : (float) w2 / w1;
|
||||
mMatrix.setScale(scale, scale);
|
||||
}
|
||||
|
||||
int saveCount = canvas.save();
|
||||
canvas.concat(mMatrix);
|
||||
d.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// OnLayout.changed isn't a reliable measure of whether or not the size of this view has changed
|
||||
// neither is onSizeChanged called often enough. Instead, we track changes in size ourselves, and
|
||||
// only invalidate this matrix if we have a new width/height spec
|
||||
if (widthMeasureSpec != mWidthSpec || heightMeasureSpec != mHeightSpec) {
|
||||
mWidthSpec = widthMeasureSpec;
|
||||
mHeightSpec = heightMeasureSpec;
|
||||
mLayoutChanged = true;
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageDrawable(Drawable drawable) {
|
||||
if (drawable == null) {
|
||||
drawable = ContextCompat.getDrawable(getContext(), R.drawable.tab_panel_tab_background);
|
||||
setScaleType(ScaleType.FIT_XY);
|
||||
mScale = false;
|
||||
} else {
|
||||
mScale = true;
|
||||
setScaleType(ScaleType.FIT_CENTER);
|
||||
}
|
||||
|
||||
super.setImageDrawable(drawable);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
/**
|
||||
* This is a copy of TouchDelegate from
|
||||
* https://github.com/android/platform_frameworks_base/blob/4b1a8f46d6ec55796bf77fd8921a5a242a219278/core/java/android/view/TouchDelegate.java
|
||||
* with a fix to reset mDelegateTargeted on each new gesture - the sole substantive change is a new
|
||||
* else leg in the ACTION_DOWN case of onTouchEvent marked by "START|END BUG FIX" comments.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper class to handle situations where you want a view to have a larger touch area than its
|
||||
* actual view bounds. The view whose touch area is changed is called the delegate view. This
|
||||
* class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
|
||||
* instance that specifies the bounds that should be mapped to the delegate and the delegate
|
||||
* view itself.
|
||||
* <p>
|
||||
* The ancestor should then forward all of its touch events received in its
|
||||
* {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
|
||||
* </p>
|
||||
*/
|
||||
public class TouchDelegateWithReset extends TouchDelegate {
|
||||
|
||||
/**
|
||||
* View that should receive forwarded touch events
|
||||
*/
|
||||
private View mDelegateView;
|
||||
|
||||
/**
|
||||
* Bounds in local coordinates of the containing view that should be mapped to the delegate
|
||||
* view. This rect is used for initial hit testing.
|
||||
*/
|
||||
private Rect mBounds;
|
||||
|
||||
/**
|
||||
* mBounds inflated to include some slop. This rect is to track whether the motion events
|
||||
* should be considered to be be within the delegate view.
|
||||
*/
|
||||
private Rect mSlopBounds;
|
||||
|
||||
/**
|
||||
* True if the delegate had been targeted on a down event (intersected mBounds).
|
||||
*/
|
||||
private boolean mDelegateTargeted;
|
||||
|
||||
private int mSlop;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param bounds Bounds in local coordinates of the containing view that should be mapped to
|
||||
* the delegate view
|
||||
* @param delegateView The view that should receive motion events
|
||||
*/
|
||||
public TouchDelegateWithReset(Rect bounds, View delegateView) {
|
||||
super(bounds, delegateView);
|
||||
|
||||
mBounds = bounds;
|
||||
|
||||
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
|
||||
mSlopBounds = new Rect(bounds);
|
||||
mSlopBounds.inset(-mSlop, -mSlop);
|
||||
mDelegateView = delegateView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will forward touch events to the delegate view if the event is within the bounds
|
||||
* specified in the constructor.
|
||||
*
|
||||
* @param event The touch event to forward
|
||||
* @return True if the event was forwarded to the delegate, false otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
int x = (int)event.getX();
|
||||
int y = (int)event.getY();
|
||||
boolean sendToDelegate = false;
|
||||
boolean hit = true;
|
||||
boolean handled = false;
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
Rect bounds = mBounds;
|
||||
|
||||
if (bounds.contains(x, y)) {
|
||||
mDelegateTargeted = true;
|
||||
sendToDelegate = true;
|
||||
} /* START BUG FIX */
|
||||
else {
|
||||
mDelegateTargeted = false;
|
||||
}
|
||||
/* END BUG FIX */
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
sendToDelegate = mDelegateTargeted;
|
||||
if (sendToDelegate) {
|
||||
Rect slopBounds = mSlopBounds;
|
||||
if (!slopBounds.contains(x, y)) {
|
||||
hit = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
sendToDelegate = mDelegateTargeted;
|
||||
mDelegateTargeted = false;
|
||||
break;
|
||||
}
|
||||
if (sendToDelegate) {
|
||||
final View delegateView = mDelegateView;
|
||||
|
||||
if (hit) {
|
||||
// Offset event coordinates to be inside the target view
|
||||
event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
|
||||
} else {
|
||||
// Offset event coordinates to be outside the target view (in case it does
|
||||
// something like tracking pressed state)
|
||||
int slop = mSlop;
|
||||
event.setLocation(-(slop * 2), -(slop * 2));
|
||||
}
|
||||
handled = delegateView.dispatchTouchEvent(event);
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
7191
mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java
Normal file
7191
mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,172 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedEditText extends android.widget.EditText
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThemedEditText(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedFrameLayout extends android.widget.FrameLayout
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedFrameLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThemedFrameLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedImageButton extends android.widget.ImageButton
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedImageButton(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThemedImageButton(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
|
||||
final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
|
||||
drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
|
||||
themedA.recycle();
|
||||
|
||||
// Apply the tint initially - the Drawable is
|
||||
// initially set by XML via super's constructor.
|
||||
setTintedImageDrawable(getDrawable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageDrawable(final Drawable drawable) {
|
||||
setTintedImageDrawable(drawable);
|
||||
}
|
||||
|
||||
private void setTintedImageDrawable(final Drawable drawable) {
|
||||
final Drawable tintedDrawable;
|
||||
if (drawableColors == null || R.id.bookmark == getId()) {
|
||||
// NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted.
|
||||
// NB: If we tint a drawable with a null ColorStateList, it will override
|
||||
// any existing colorFilters and tint... so don't!
|
||||
tintedDrawable = drawable;
|
||||
} else if (drawable == null) {
|
||||
tintedDrawable = null;
|
||||
} else {
|
||||
tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
|
||||
}
|
||||
super.setImageDrawable(tintedDrawable);
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedImageView extends android.widget.ImageView
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThemedImageView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
|
||||
final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
|
||||
drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
|
||||
themedA.recycle();
|
||||
|
||||
// Apply the tint initially - the Drawable is
|
||||
// initially set by XML via super's constructor.
|
||||
setTintedImageDrawable(getDrawable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageDrawable(final Drawable drawable) {
|
||||
setTintedImageDrawable(drawable);
|
||||
}
|
||||
|
||||
private void setTintedImageDrawable(final Drawable drawable) {
|
||||
final Drawable tintedDrawable;
|
||||
if (drawableColors == null) {
|
||||
// NB: If we tint a drawable with a null ColorStateList, it will override
|
||||
// any existing colorFilters and tint... so don't!
|
||||
tintedDrawable = drawable;
|
||||
} else if (drawable == null) {
|
||||
tintedDrawable = null;
|
||||
} else {
|
||||
tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
|
||||
}
|
||||
super.setImageDrawable(tintedDrawable);
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedLinearLayout extends android.widget.LinearLayout
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedLinearLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedRelativeLayout extends android.widget.RelativeLayout
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedRelativeLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThemedRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedTextSwitcher extends android.widget.TextSwitcher
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedTextSwitcher(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedTextView extends android.widget.TextView
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThemedTextView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ThemedView extends android.view.View
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public ThemedView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThemedView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
//#filter substitution
|
||||
// This file is generated by generate_themed_views.py; do not edit.
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.widget.themed;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.lwt.LightweightTheme;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class Themed@VIEW_NAME_SUFFIX@ extends @BASE_TYPE@
|
||||
implements LightweightTheme.OnChangeListener {
|
||||
private LightweightTheme theme;
|
||||
|
||||
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
|
||||
private static final int[] STATE_LIGHT = { R.attr.state_light };
|
||||
private static final int[] STATE_DARK = { R.attr.state_dark };
|
||||
|
||||
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
|
||||
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
|
||||
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
|
||||
|
||||
private boolean isPrivate;
|
||||
private boolean isLight;
|
||||
private boolean isDark;
|
||||
private boolean autoUpdateTheme; // always false if there's no theme.
|
||||
|
||||
private ColorStateList drawableColors;
|
||||
|
||||
public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs, 0);
|
||||
}
|
||||
|
||||
//#ifdef STYLE_CONSTRUCTOR
|
||||
public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
//#endif
|
||||
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
// The theme can be null, particularly if we might be instantiating this
|
||||
// View in an IDE, with no ambient GeckoApplication.
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
if (applicationContext instanceof GeckoApplication) {
|
||||
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
|
||||
}
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
|
||||
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
|
||||
a.recycle();
|
||||
//#if TINT_FOREGROUND_DRAWABLE
|
||||
|
||||
final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
|
||||
drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
|
||||
themedA.recycle();
|
||||
|
||||
// Apply the tint initially - the Drawable is
|
||||
// initially set by XML via super's constructor.
|
||||
setTintedImageDrawable(getDrawable());
|
||||
//#endif
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (isPrivate)
|
||||
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
|
||||
else if (isLight)
|
||||
mergeDrawableStates(drawableState, STATE_LIGHT);
|
||||
else if (isDark)
|
||||
mergeDrawableStates(drawableState, STATE_DARK);
|
||||
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeChanged() {
|
||||
if (autoUpdateTheme && theme.isEnabled())
|
||||
setTheme(theme.isLightTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightweightThemeReset() {
|
||||
if (autoUpdateTheme)
|
||||
resetTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
onLightweightThemeChanged();
|
||||
}
|
||||
|
||||
public boolean isPrivateMode() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
if (this.isPrivate != isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(boolean isLight) {
|
||||
// Set the theme only if it is different from existing theme.
|
||||
if ((isLight && this.isLight != isLight) ||
|
||||
(!isLight && this.isDark == isLight)) {
|
||||
if (isLight) {
|
||||
this.isLight = true;
|
||||
this.isDark = false;
|
||||
} else {
|
||||
this.isLight = false;
|
||||
this.isDark = true;
|
||||
}
|
||||
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTheme() {
|
||||
if (isLight || isDark) {
|
||||
isLight = false;
|
||||
isDark = false;
|
||||
refreshDrawableState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
|
||||
if (theme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoUpdateTheme != autoUpdateTheme) {
|
||||
this.autoUpdateTheme = autoUpdateTheme;
|
||||
|
||||
if (autoUpdateTheme)
|
||||
theme.addListener(this);
|
||||
else
|
||||
theme.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
//#ifdef TINT_FOREGROUND_DRAWABLE
|
||||
@Override
|
||||
public void setImageDrawable(final Drawable drawable) {
|
||||
setTintedImageDrawable(drawable);
|
||||
}
|
||||
|
||||
private void setTintedImageDrawable(final Drawable drawable) {
|
||||
final Drawable tintedDrawable;
|
||||
//#ifdef BOOKMARK_NO_TINT
|
||||
if (drawableColors == null || R.id.bookmark == getId()) {
|
||||
// NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted.
|
||||
//#else
|
||||
if (drawableColors == null) {
|
||||
//#endif
|
||||
// NB: If we tint a drawable with a null ColorStateList, it will override
|
||||
// any existing colorFilters and tint... so don't!
|
||||
tintedDrawable = drawable;
|
||||
} else if (drawable == null) {
|
||||
tintedDrawable = null;
|
||||
} else {
|
||||
tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
|
||||
}
|
||||
super.setImageDrawable(tintedDrawable);
|
||||
}
|
||||
|
||||
//#endif
|
||||
public ColorDrawable getColorDrawable(int id) {
|
||||
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
|
||||
}
|
||||
|
||||
protected LightweightTheme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/python
|
||||
|
||||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
'''
|
||||
Script to generate Themed*.java source files for Fennec.
|
||||
|
||||
This script runs the preprocessor on a input template and writes
|
||||
updated files into the source directory.
|
||||
|
||||
To update the themed views, update the input template
|
||||
(ThemedView.java.frag) and run the script using 'mach python <script.py>'. Use version control to
|
||||
examine the differences, and don't forget to commit the changes to the
|
||||
template and the outputs.
|
||||
'''
|
||||
|
||||
from __future__ import (
|
||||
print_function,
|
||||
unicode_literals,
|
||||
)
|
||||
|
||||
import os
|
||||
|
||||
from mozbuild.preprocessor import Preprocessor
|
||||
|
||||
__DIR__ = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
template = os.path.join(__DIR__, 'ThemedView.java.frag')
|
||||
dest_format_string = 'Themed%(VIEW_NAME_SUFFIX)s.java'
|
||||
|
||||
views = [
|
||||
dict(VIEW_NAME_SUFFIX='EditText',
|
||||
BASE_TYPE='android.widget.EditText',
|
||||
STYLE_CONSTRUCTOR=1),
|
||||
dict(VIEW_NAME_SUFFIX='FrameLayout',
|
||||
BASE_TYPE='android.widget.FrameLayout',
|
||||
STYLE_CONSTRUCTOR=1),
|
||||
dict(VIEW_NAME_SUFFIX='ImageButton',
|
||||
BASE_TYPE='android.widget.ImageButton',
|
||||
STYLE_CONSTRUCTOR=1,
|
||||
TINT_FOREGROUND_DRAWABLE=1,
|
||||
BOOKMARK_NO_TINT=1),
|
||||
dict(VIEW_NAME_SUFFIX='ImageView',
|
||||
BASE_TYPE='android.widget.ImageView',
|
||||
STYLE_CONSTRUCTOR=1,
|
||||
TINT_FOREGROUND_DRAWABLE=1),
|
||||
dict(VIEW_NAME_SUFFIX='LinearLayout',
|
||||
BASE_TYPE='android.widget.LinearLayout'),
|
||||
dict(VIEW_NAME_SUFFIX='RelativeLayout',
|
||||
BASE_TYPE='android.widget.RelativeLayout',
|
||||
STYLE_CONSTRUCTOR=1),
|
||||
dict(VIEW_NAME_SUFFIX='TextSwitcher',
|
||||
BASE_TYPE='android.widget.TextSwitcher'),
|
||||
dict(VIEW_NAME_SUFFIX='TextView',
|
||||
BASE_TYPE='android.widget.TextView',
|
||||
STYLE_CONSTRUCTOR=1),
|
||||
dict(VIEW_NAME_SUFFIX='View',
|
||||
BASE_TYPE='android.view.View',
|
||||
STYLE_CONSTRUCTOR=1),
|
||||
]
|
||||
|
||||
for view in views:
|
||||
pp = Preprocessor(defines=view, marker='//#')
|
||||
|
||||
dest = os.path.join(__DIR__, dest_format_string % view)
|
||||
with open(template, 'rU') as input:
|
||||
with open(dest, 'wt') as output:
|
||||
pp.processFile(input=input, output=output)
|
||||
print('%s' % dest)
|
||||
Loading…
Add table
Add a link
Reference in a new issue