diff options
Diffstat (limited to 'java/com/android/contacts/common/widget')
4 files changed, 605 insertions, 0 deletions
diff --git a/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java new file mode 100644 index 000000000..2988a5a58 --- /dev/null +++ b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; +import com.android.dialer.util.TouchPointManager; + +/** + * Linear layout for an activity that listens to all touch events on the screen and saves the touch + * point. Typically touch events are handled by child views--this class intercepts those touch + * events before passing them on to the child. + */ +public class ActivityTouchLinearLayout extends LinearLayout { + + public ActivityTouchLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return false; + } +} diff --git a/java/com/android/contacts/common/widget/FloatingActionButtonController.java b/java/com/android/contacts/common/widget/FloatingActionButtonController.java new file mode 100644 index 000000000..f03129779 --- /dev/null +++ b/java/com/android/contacts/common/widget/FloatingActionButtonController.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.app.Activity; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.ImageButton; +import com.android.contacts.common.R; +import com.android.contacts.common.util.FabUtil; +import com.android.dialer.animation.AnimUtils; + +/** Controls the movement and appearance of the FAB (Floating Action Button). */ +public class FloatingActionButtonController { + + public static final int ALIGN_MIDDLE = 0; + public static final int ALIGN_QUARTER_END = 1; + public static final int ALIGN_END = 2; + + private static final int FAB_SCALE_IN_DURATION = 266; + private static final int FAB_SCALE_IN_FADE_IN_DELAY = 100; + private static final int FAB_ICON_FADE_OUT_DURATION = 66; + + private final int mAnimationDuration; + private final int mFloatingActionButtonWidth; + private final int mFloatingActionButtonMarginRight; + private final View mFloatingActionButtonContainer; + private final ImageButton mFloatingActionButton; + private final Interpolator mFabInterpolator; + private int mScreenWidth; + + public FloatingActionButtonController(Activity activity, View container, ImageButton button) { + Resources resources = activity.getResources(); + mFabInterpolator = + AnimationUtils.loadInterpolator(activity, android.R.interpolator.fast_out_slow_in); + mFloatingActionButtonWidth = + resources.getDimensionPixelSize(R.dimen.floating_action_button_width); + mFloatingActionButtonMarginRight = + resources.getDimensionPixelOffset(R.dimen.floating_action_button_margin_right); + mAnimationDuration = resources.getInteger(R.integer.floating_action_button_animation_duration); + mFloatingActionButtonContainer = container; + mFloatingActionButton = button; + FabUtil.setupFloatingActionButton(mFloatingActionButtonContainer, resources); + } + + /** + * Passes the screen width into the class. Necessary for translation calculations. Should be + * called as soon as parent View width is available. + * + * @param screenWidth The width of the screen in pixels. + */ + public void setScreenWidth(int screenWidth) { + mScreenWidth = screenWidth; + } + + public boolean isVisible() { + return mFloatingActionButtonContainer.getVisibility() == View.VISIBLE; + } + + /** + * Sets FAB as View.VISIBLE or View.GONE. + * + * @param visible Whether or not to make the container visible. + */ + public void setVisible(boolean visible) { + mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public void changeIcon(Drawable icon, String description) { + if (mFloatingActionButton.getDrawable() != icon + || !mFloatingActionButton.getContentDescription().equals(description)) { + mFloatingActionButton.setImageDrawable(icon); + mFloatingActionButton.setContentDescription(description); + } + } + + /** + * Updates the FAB location (middle to right position) as the PageView scrolls. + * + * @param positionOffset A fraction used to calculate position of the FAB during page scroll. + */ + public void onPageScrolled(float positionOffset) { + // As the page is scrolling, if we're on the first tab, update the FAB position so it + // moves along with it. + mFloatingActionButtonContainer.setTranslationX( + (int) (positionOffset * getTranslationXForAlignment(ALIGN_END))); + } + + /** + * Aligns the FAB to the described location + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @param animate Whether or not to animate the transition. + */ + public void align(int align, boolean animate) { + align(align, 0 /*offsetX */, 0 /* offsetY */, animate); + } + + /** + * Aligns the FAB to the described location plus specified additional offsets. + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @param offsetX Additional offsetX to translate by. + * @param offsetY Additional offsetY to translate by. + * @param animate Whether or not to animate the transition. + */ + public void align(int align, int offsetX, int offsetY, boolean animate) { + if (mScreenWidth == 0) { + return; + } + + int translationX = getTranslationXForAlignment(align); + + // Skip animation if container is not shown; animation causes container to show again. + if (animate && mFloatingActionButtonContainer.isShown()) { + mFloatingActionButtonContainer + .animate() + .translationX(translationX + offsetX) + .translationY(offsetY) + .setInterpolator(mFabInterpolator) + .setDuration(mAnimationDuration) + .start(); + } else { + mFloatingActionButtonContainer.setTranslationX(translationX + offsetX); + mFloatingActionButtonContainer.setTranslationY(offsetY); + } + } + + /** + * Resizes width and height of the floating action bar container. + * + * @param dimension The new dimensions for the width and height. + * @param animate Whether to animate this change. + */ + public void resize(int dimension, boolean animate) { + if (animate) { + AnimUtils.changeDimensions(mFloatingActionButtonContainer, dimension, dimension); + } else { + mFloatingActionButtonContainer.getLayoutParams().width = dimension; + mFloatingActionButtonContainer.getLayoutParams().height = dimension; + mFloatingActionButtonContainer.requestLayout(); + } + } + + /** + * Scales the floating action button from no height and width to its actual dimensions. This is an + * animation for showing the floating action button. + * + * @param delayMs The delay for the effect, in milliseconds. + */ + public void scaleIn(int delayMs) { + setVisible(true); + AnimUtils.scaleIn(mFloatingActionButtonContainer, FAB_SCALE_IN_DURATION, delayMs); + AnimUtils.fadeIn( + mFloatingActionButton, FAB_SCALE_IN_DURATION, delayMs + FAB_SCALE_IN_FADE_IN_DELAY, null); + } + + /** Immediately remove the affects of the last call to {@link #scaleOut}. */ + public void resetIn() { + mFloatingActionButton.setAlpha(1f); + mFloatingActionButton.setVisibility(View.VISIBLE); + mFloatingActionButtonContainer.setScaleX(1); + mFloatingActionButtonContainer.setScaleY(1); + } + + /** + * Scales the floating action button from its actual dimensions to no height and width. This is an + * animation for hiding the floating action button. + */ + public void scaleOut() { + AnimUtils.scaleOut(mFloatingActionButtonContainer, mAnimationDuration); + // Fade out the icon faster than the scale out animation, so that the icon scaling is less + // obvious. We don't want it to scale, but the resizing the container is not as performant. + AnimUtils.fadeOut(mFloatingActionButton, FAB_ICON_FADE_OUT_DURATION, null); + } + + /** + * Calculates the X offset of the FAB to the given alignment, adjusted for whether or not the view + * is in RTL mode. + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @return The translationX for the given alignment. + */ + public int getTranslationXForAlignment(int align) { + int result = 0; + switch (align) { + case ALIGN_MIDDLE: + // Moves the FAB to exactly center screen. + return 0; + case ALIGN_QUARTER_END: + // Moves the FAB a quarter of the screen width. + result = mScreenWidth / 4; + break; + case ALIGN_END: + // Moves the FAB half the screen width. Same as aligning right with a marginRight. + result = + mScreenWidth / 2 - mFloatingActionButtonWidth / 2 - mFloatingActionButtonMarginRight; + break; + } + if (isLayoutRtl()) { + result *= -1; + } + return result; + } + + private boolean isLayoutRtl() { + return mFloatingActionButtonContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } +} diff --git a/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java new file mode 100644 index 000000000..d84d8f757 --- /dev/null +++ b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * Custom {@link ImageView} that improves layouting performance. + * + * <p>This improves the performance by not passing requestLayout() to its parent, taking advantage + * of knowing that image size won't change once set. + */ +public class LayoutSuppressingImageView extends ImageView { + + public LayoutSuppressingImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void requestLayout() { + forceLayout(); + } +} diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java new file mode 100644 index 000000000..63f8ca580 --- /dev/null +++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.widget; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.TextView; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneAccountCompat; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import java.util.ArrayList; +import java.util.List; + +/** + * Dialog that allows the user to select a phone accounts for a given action. Optionally provides + * the choice to set the phone account as default. + */ +public class SelectPhoneAccountDialogFragment extends DialogFragment { + + private static final String ARG_TITLE_RES_ID = "title_res_id"; + private static final String ARG_CAN_SET_DEFAULT = "can_set_default"; + private static final String ARG_ACCOUNT_HANDLES = "account_handles"; + private static final String ARG_IS_DEFAULT_CHECKED = "is_default_checked"; + private static final String ARG_LISTENER = "listener"; + private static final String ARG_CALL_ID = "call_id"; + + private int mTitleResId; + private boolean mCanSetDefault; + private List<PhoneAccountHandle> mAccountHandles; + private boolean mIsSelected; + private boolean mIsDefaultChecked; + private SelectPhoneAccountListener mListener; + + public SelectPhoneAccountDialogFragment() {} + + /** + * Create new fragment instance with default title and no option to set as default. + * + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static SelectPhoneAccountDialogFragment newInstance( + List<PhoneAccountHandle> accountHandles, + SelectPhoneAccountListener listener, + @Nullable String callId) { + return newInstance( + R.string.select_account_dialog_title, false, accountHandles, listener, callId); + } + + /** + * Create new fragment instance. This method also allows specifying a custom title and "set + * default" checkbox. + * + * @param titleResId The resource ID for the string to use in the title of the dialog. + * @param canSetDefault {@code true} if the dialog should include an option to set the selection + * as the default. False otherwise. + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static SelectPhoneAccountDialogFragment newInstance( + int titleResId, + boolean canSetDefault, + List<PhoneAccountHandle> accountHandles, + SelectPhoneAccountListener listener, + @Nullable String callId) { + ArrayList<PhoneAccountHandle> accountHandlesCopy = new ArrayList<>(); + if (accountHandles != null) { + accountHandlesCopy.addAll(accountHandles); + } + SelectPhoneAccountDialogFragment fragment = new SelectPhoneAccountDialogFragment(); + final Bundle args = new Bundle(); + args.putInt(ARG_TITLE_RES_ID, titleResId); + args.putBoolean(ARG_CAN_SET_DEFAULT, canSetDefault); + args.putParcelableArrayList(ARG_ACCOUNT_HANDLES, accountHandlesCopy); + args.putParcelable(ARG_LISTENER, listener); + args.putString(ARG_CALL_ID, callId); + fragment.setArguments(args); + fragment.setListener(listener); + return fragment; + } + + public void setListener(SelectPhoneAccountListener listener) { + mListener = listener; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(ARG_IS_DEFAULT_CHECKED, mIsDefaultChecked); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID); + mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT); + mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES); + mListener = getArguments().getParcelable(ARG_LISTENER); + if (savedInstanceState != null) { + mIsDefaultChecked = savedInstanceState.getBoolean(ARG_IS_DEFAULT_CHECKED); + } + mIsSelected = false; + + final DialogInterface.OnClickListener selectionListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mIsSelected = true; + PhoneAccountHandle selectedAccountHandle = mAccountHandles.get(which); + Bundle result = new Bundle(); + result.putParcelable( + SelectPhoneAccountListener.EXTRA_SELECTED_ACCOUNT_HANDLE, selectedAccountHandle); + result.putBoolean(SelectPhoneAccountListener.EXTRA_SET_DEFAULT, mIsDefaultChecked); + result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId()); + if (mListener != null) { + mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_SELECTED, result); + } + } + }; + + final CompoundButton.OnCheckedChangeListener checkListener = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton check, boolean isChecked) { + mIsDefaultChecked = isChecked; + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + ListAdapter selectAccountListAdapter = + new SelectAccountListAdapter( + builder.getContext(), R.layout.select_account_list_item, mAccountHandles); + + AlertDialog dialog = + builder + .setTitle(mTitleResId) + .setAdapter(selectAccountListAdapter, selectionListener) + .create(); + + if (mCanSetDefault) { + // Generate custom checkbox view, lint suppressed since no appropriate parent (is dialog) + @SuppressLint("InflateParams") + LinearLayout checkboxLayout = + (LinearLayout) + LayoutInflater.from(builder.getContext()) + .inflate(R.layout.default_account_checkbox, null); + + CheckBox cb = (CheckBox) checkboxLayout.findViewById(R.id.default_account_checkbox_view); + cb.setOnCheckedChangeListener(checkListener); + cb.setChecked(mIsDefaultChecked); + + dialog.getListView().addFooterView(checkboxLayout); + } + + return dialog; + } + + @Override + public void onStop() { + if (!mIsSelected && mListener != null) { + Bundle result = new Bundle(); + result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId()); + mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, result); + } + super.onStop(); + } + + @Nullable + private String getCallId() { + return getArguments().getString(ARG_CALL_ID); + } + + public static class SelectPhoneAccountListener extends ResultReceiver { + + static final int RESULT_SELECTED = 1; + static final int RESULT_DISMISSED = 2; + + static final String EXTRA_SELECTED_ACCOUNT_HANDLE = "extra_selected_account_handle"; + static final String EXTRA_SET_DEFAULT = "extra_set_default"; + static final String EXTRA_CALL_ID = "extra_call_id"; + + public SelectPhoneAccountListener() { + super(new Handler()); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == RESULT_SELECTED) { + onPhoneAccountSelected( + resultData.getParcelable(EXTRA_SELECTED_ACCOUNT_HANDLE), + resultData.getBoolean(EXTRA_SET_DEFAULT), + resultData.getString(EXTRA_CALL_ID)); + } else if (resultCode == RESULT_DISMISSED) { + onDialogDismissed(resultData.getString(EXTRA_CALL_ID)); + } + } + + public void onPhoneAccountSelected( + PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {} + + public void onDialogDismissed(@Nullable String callId) {} + } + + private static class SelectAccountListAdapter extends ArrayAdapter<PhoneAccountHandle> { + + private int mResId; + + public SelectAccountListAdapter( + Context context, int resource, List<PhoneAccountHandle> accountHandles) { + super(context, resource, accountHandles); + mResId = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + View rowView; + final ViewHolder holder; + + if (convertView == null) { + // Cache views for faster scrolling + rowView = inflater.inflate(mResId, null); + holder = new ViewHolder(); + holder.labelTextView = (TextView) rowView.findViewById(R.id.label); + holder.numberTextView = (TextView) rowView.findViewById(R.id.number); + holder.imageView = (ImageView) rowView.findViewById(R.id.icon); + rowView.setTag(holder); + } else { + rowView = convertView; + holder = (ViewHolder) rowView.getTag(); + } + + PhoneAccountHandle accountHandle = getItem(position); + PhoneAccount account = + getContext().getSystemService(TelecomManager.class).getPhoneAccount(accountHandle); + if (account == null) { + return rowView; + } + holder.labelTextView.setText(account.getLabel()); + if (account.getAddress() == null + || TextUtils.isEmpty(account.getAddress().getSchemeSpecificPart())) { + holder.numberTextView.setVisibility(View.GONE); + } else { + holder.numberTextView.setVisibility(View.VISIBLE); + holder.numberTextView.setText( + PhoneNumberUtilsCompat.createTtsSpannable( + account.getAddress().getSchemeSpecificPart())); + } + holder.imageView.setImageDrawable( + PhoneAccountCompat.createIconDrawable(account, getContext())); + return rowView; + } + + private static final class ViewHolder { + + TextView labelTextView; + TextView numberTextView; + ImageView imageView; + } + } +} |