Update the sample keyboard to showcase scroll, clip, toggle am: f34c6275f1
Change-Id: I5a636d05e66959a4d627dfb41ebd7a85e913d939
This commit is contained in:
@@ -22,14 +22,43 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/KeyboardArea">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
<LinearLayout
|
||||
android:id="@+id/suggestion_strip"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/suggestion_view"
|
||||
style="@style/KeyboardRow.Header">
|
||||
android:id="@+id/pinned_suggestions_start"
|
||||
style="@style/PinnedSuggestionArea">
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.example.android.autofillkeyboard.InlineContentClipView
|
||||
android:id="@+id/scrollable_suggestions_clip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/scrollable_suggestions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/KeyboardRow.Header">
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</com.example.android.autofillkeyboard.InlineContentClipView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pinned_suggestions_end"
|
||||
style="@style/PinnedSuggestionArea">
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/KeyboardRow">
|
||||
<TextView
|
||||
@@ -141,4 +170,5 @@
|
||||
android:id="@+id/key_pos_enter"
|
||||
style="@style/SoftKey.Function"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
19
samples/AutofillKeyboard/res/values/colors.xml
Normal file
19
samples/AutofillKeyboard/res/values/colors.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2020 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<color name="suggestion_strip_background">#FFEEEEEE</color>
|
||||
</resources>
|
||||
@@ -22,6 +22,6 @@
|
||||
<dimen name="text_size_normal">24dp</dimen>
|
||||
<dimen name="text_size_symbol">14dp</dimen>
|
||||
|
||||
<dimen name="keyboard_header_height">41dp</dimen>
|
||||
<dimen name="keyboard_header_height">48dp</dimen>
|
||||
<dimen name="keyboard_row_height">48dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -25,18 +25,26 @@
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_gravity">bottom</item>
|
||||
<item name="android:orientation">vertical</item>
|
||||
<item name="android:background">#FFFFFFFF</item>
|
||||
<item name="android:gravity">bottom</item>
|
||||
</style>
|
||||
|
||||
<style name="KeyboardRow">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">@dimen/keyboard_row_height</item>
|
||||
<item name="android:orientation">horizontal</item>
|
||||
<item name="android:background">#FFFFFFFF</item>
|
||||
</style>
|
||||
|
||||
<style name="PinnedSuggestionArea">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">@dimen/keyboard_header_height</item>
|
||||
<item name="android:layout_weight">0</item>
|
||||
<item name="android:background">@color/suggestion_strip_background</item>
|
||||
</style>
|
||||
|
||||
<style name="KeyboardRow.Header">
|
||||
<item name="android:layout_height">@dimen/keyboard_header_height</item>
|
||||
<item name="android:background">#FFEEEEEE</item>
|
||||
<item name="android:background">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="SoftKey">
|
||||
|
||||
@@ -17,34 +17,73 @@
|
||||
package com.example.android.autofillkeyboard;
|
||||
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.GuardedBy;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.inline.InlinePresentationSpec;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.inline.InlineContentView;
|
||||
import android.widget.inline.InlinePresentationSpec;
|
||||
import android.view.inputmethod.InlineSuggestion;
|
||||
import android.view.inputmethod.InlineSuggestionsRequest;
|
||||
import android.view.inputmethod.InlineSuggestionsResponse;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** The {@link InputMethodService} implementation for Autofill keyboard. */
|
||||
public class AutofillImeService extends InputMethodService {
|
||||
private static final boolean SHOWCASE_BG_FG_TRANSITION = false;
|
||||
// To test this you need to change KeyboardArea style layout_height to 400dp
|
||||
private static final boolean SHOWCASE_UP_DOWN_TRANSITION = false;
|
||||
|
||||
private static final long MOVE_SUGGESTIONS_TO_BG_TIMEOUT = 5000;
|
||||
private static final long MOVE_SUGGESTIONS_TO_FG_TIMEOUT = 15000;
|
||||
|
||||
private static final long MOVE_SUGGESTIONS_UP_TIMEOUT = 5000;
|
||||
private static final long MOVE_SUGGESTIONS_DOWN_TIMEOUT = 10000;
|
||||
|
||||
private InputView mInputView;
|
||||
private Decoder mDecoder;
|
||||
private LinearLayout mSuggestionStrip;
|
||||
|
||||
private ViewGroup mSuggestionStrip;
|
||||
private ViewGroup mPinnedSuggestionsStart;
|
||||
private ViewGroup mPinnedSuggestionsEnd;
|
||||
private InlineContentClipView mScrollableSuggestionsClip;
|
||||
private ViewGroup mScrollableSuggestions;
|
||||
|
||||
private final Runnable mMoveScrollableSuggestionsToBg = () -> {
|
||||
mScrollableSuggestionsClip.setZOrderedOnTop(false);
|
||||
Toast.makeText(AutofillImeService.this, "Chips moved to bg - not clickable",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
};
|
||||
|
||||
private final Runnable mMoveScrollableSuggestionsToFg = () -> {
|
||||
mScrollableSuggestionsClip.setZOrderedOnTop(true);
|
||||
Toast.makeText(AutofillImeService.this, "Chips moved to fg - clickable",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
};
|
||||
|
||||
private final Runnable mMoveScrollableSuggestionsUp = () -> {
|
||||
mSuggestionStrip.animate().translationY(-50).setDuration(500).start();
|
||||
Toast.makeText(AutofillImeService.this, "Animating up",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
};
|
||||
|
||||
private final Runnable mMoveScrollableSuggestionsDown = () -> {
|
||||
mSuggestionStrip.animate().translationY(0).setDuration(500).start();
|
||||
Toast.makeText(AutofillImeService.this, "Animating down",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateInputView() {
|
||||
@@ -61,10 +100,18 @@ public class AutofillImeService extends InputMethodService {
|
||||
@Override
|
||||
public void onStartInputView(EditorInfo info, boolean restarting) {
|
||||
super.onStartInputView(info, restarting);
|
||||
|
||||
mInputView.removeAllViews();
|
||||
Keyboard keyboard = Keyboard.qwerty(this);
|
||||
mInputView.addView(keyboard.inflateKeyboardView(LayoutInflater.from(this), mInputView));
|
||||
mSuggestionStrip = mInputView.findViewById(R.id.suggestion_view);
|
||||
|
||||
mSuggestionStrip = mInputView.findViewById(R.id.suggestion_strip);
|
||||
mPinnedSuggestionsStart = mInputView.findViewById(R.id.pinned_suggestions_start);
|
||||
mPinnedSuggestionsEnd = mInputView.findViewById(R.id.pinned_suggestions_end);
|
||||
mScrollableSuggestionsClip = mInputView.findViewById(R.id.scrollable_suggestions_clip);
|
||||
mScrollableSuggestions = mInputView.findViewById(R.id.scrollable_suggestions);
|
||||
|
||||
updateInlineSuggestionStrip(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,15 +127,6 @@ public class AutofillImeService extends InputMethodService {
|
||||
|
||||
private static final String TAG = "AutofillImeService";
|
||||
|
||||
private Handler mMainHandler = new Handler();
|
||||
|
||||
@GuardedBy("this")
|
||||
private List<View> mSuggestionViews = new ArrayList<>();
|
||||
@GuardedBy("this")
|
||||
private List<Size> mSuggestionViewSizes = new ArrayList<>();
|
||||
@GuardedBy("this")
|
||||
private boolean mSuggestionViewVisible = false;
|
||||
|
||||
@Override
|
||||
public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(Bundle uiExtras) {
|
||||
Log.d(TAG, "onCreateInlineSuggestionsRequest() called");
|
||||
@@ -106,45 +144,65 @@ public class AutofillImeService extends InputMethodService {
|
||||
@Override
|
||||
public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
|
||||
Log.d(TAG, "onInlineSuggestionsResponse() called");
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
onInlineSuggestionsResponseInternal(response);
|
||||
});
|
||||
onInlineSuggestionsResponseInternal(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
private synchronized void updateInlineSuggestionVisibility(boolean visible, boolean force) {
|
||||
Log.d(TAG, "updateInlineSuggestionVisibility() called, visible=" + visible + ", force="
|
||||
+ force);
|
||||
mMainHandler.post(() -> {
|
||||
Log.d(TAG, "updateInlineSuggestionVisibility() running");
|
||||
if (visible == mSuggestionViewVisible && !force) {
|
||||
return;
|
||||
} else if (visible) {
|
||||
mSuggestionStrip.removeAllViews();
|
||||
final int size = mSuggestionViews.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if(mSuggestionViews.get(i) == null) {
|
||||
continue;
|
||||
}
|
||||
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
|
||||
mSuggestionViewSizes.get(i).getWidth(),
|
||||
mSuggestionViewSizes.get(i).getHeight());
|
||||
mSuggestionStrip.addView(mSuggestionViews.get(i), layoutParams);
|
||||
}
|
||||
mSuggestionViewVisible = true;
|
||||
} else {
|
||||
mSuggestionStrip.removeAllViews();
|
||||
mSuggestionViewVisible = false;
|
||||
private void updateInlineSuggestionStrip(List<SuggestionItem> suggestionItems) {
|
||||
mPinnedSuggestionsStart.removeAllViews();
|
||||
mScrollableSuggestions.removeAllViews();
|
||||
mPinnedSuggestionsEnd.removeAllViews();
|
||||
|
||||
final int size = suggestionItems.size();
|
||||
if (size <= 0) {
|
||||
mSuggestionStrip.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: refactor me
|
||||
mScrollableSuggestionsClip.setBackgroundColor(
|
||||
getColor(R.color.suggestion_strip_background));
|
||||
mSuggestionStrip.setVisibility(View.VISIBLE);
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
final SuggestionItem suggestionItem = suggestionItems.get(i);
|
||||
if (suggestionItem == null) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
final InlineContentView suggestionView = suggestionItem.mView;
|
||||
if (suggestionItem.mIsPinned) {
|
||||
if (mPinnedSuggestionsStart.getChildCount() <= 0) {
|
||||
mPinnedSuggestionsStart.addView(suggestionView);
|
||||
} else {
|
||||
mPinnedSuggestionsEnd.addView(suggestionView);
|
||||
}
|
||||
} else {
|
||||
mScrollableSuggestions.addView(suggestionView);
|
||||
}
|
||||
}
|
||||
|
||||
if (SHOWCASE_BG_FG_TRANSITION) {
|
||||
rescheduleShowcaseBgFgTransitions();
|
||||
}
|
||||
if (SHOWCASE_UP_DOWN_TRANSITION) {
|
||||
rescheduleShowcaseUpDownTransitions();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void updateSuggestionViews(View[] suggestionViews, Size[] sizes) {
|
||||
Log.d(TAG, "updateSuggestionViews() called");
|
||||
mSuggestionViews = Arrays.asList(suggestionViews);
|
||||
mSuggestionViewSizes = Arrays.asList(sizes);
|
||||
final boolean visible = !mSuggestionViews.isEmpty();
|
||||
updateInlineSuggestionVisibility(visible, true);
|
||||
private void rescheduleShowcaseBgFgTransitions() {
|
||||
final Handler handler = mInputView.getHandler();
|
||||
handler.removeCallbacks(mMoveScrollableSuggestionsToBg);
|
||||
handler.postDelayed(mMoveScrollableSuggestionsToBg, MOVE_SUGGESTIONS_TO_BG_TIMEOUT);
|
||||
handler.removeCallbacks(mMoveScrollableSuggestionsToFg);
|
||||
handler.postDelayed(mMoveScrollableSuggestionsToFg, MOVE_SUGGESTIONS_TO_FG_TIMEOUT);
|
||||
}
|
||||
|
||||
private void rescheduleShowcaseUpDownTransitions() {
|
||||
final Handler handler = mInputView.getHandler();
|
||||
handler.removeCallbacks(mMoveScrollableSuggestionsUp);
|
||||
handler.postDelayed(mMoveScrollableSuggestionsUp, MOVE_SUGGESTIONS_UP_TIMEOUT);
|
||||
handler.removeCallbacks(mMoveScrollableSuggestionsDown);
|
||||
handler.postDelayed(mMoveScrollableSuggestionsDown, MOVE_SUGGESTIONS_DOWN_TIMEOUT);
|
||||
}
|
||||
|
||||
private void onInlineSuggestionsResponseInternal(InlineSuggestionsResponse response) {
|
||||
@@ -152,38 +210,48 @@ public class AutofillImeService extends InputMethodService {
|
||||
+ response.getInlineSuggestions().size());
|
||||
|
||||
final List<InlineSuggestion> inlineSuggestions = response.getInlineSuggestions();
|
||||
final int totalSuggestionsCount = inlineSuggestions.size();
|
||||
final AtomicInteger suggestionsCount = new AtomicInteger(totalSuggestionsCount);
|
||||
final View[] suggestionViews = new View[totalSuggestionsCount];
|
||||
final Size[] sizes = new Size[totalSuggestionsCount];
|
||||
|
||||
if (totalSuggestionsCount == 0) {
|
||||
updateSuggestionViews(suggestionViews, sizes);
|
||||
final int totalSuggestionsCount = inlineSuggestions.size();
|
||||
if (totalSuggestionsCount <= 0) {
|
||||
updateInlineSuggestionStrip(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
for (int i=0; i<totalSuggestionsCount; i++) {
|
||||
|
||||
final Map<Integer, SuggestionItem> suggestionMap = Collections.synchronizedMap((
|
||||
new TreeMap<>()));
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
for (int i = 0; i < totalSuggestionsCount; i++) {
|
||||
final int index = i;
|
||||
InlineSuggestion inlineSuggestion = inlineSuggestions.get(index);
|
||||
Size size = inlineSuggestion.getInfo().getInlinePresentationSpec().getMaxSize();
|
||||
inlineSuggestion.inflate(this, size,
|
||||
AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
suggestionView -> {
|
||||
Log.d(TAG, "new inline suggestion view ready");
|
||||
if(suggestionView != null) {
|
||||
suggestionViews[index] = suggestionView;
|
||||
sizes[index] = size;
|
||||
suggestionView.setOnClickListener((v) -> {
|
||||
Log.d(TAG, "Received click on the suggestion");
|
||||
});
|
||||
suggestionView.setOnLongClickListener((v) -> {
|
||||
Log.d(TAG, "Received long click on the suggestion");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (suggestionsCount.decrementAndGet() == 0) {
|
||||
updateSuggestionViews(suggestionViews, sizes);
|
||||
}
|
||||
final InlineSuggestion inlineSuggestion = inlineSuggestions.get(i);
|
||||
final Size size = inlineSuggestion.getInfo().getInlinePresentationSpec().getMaxSize();
|
||||
|
||||
inlineSuggestion.inflate(this, size, executor, suggestionView -> {
|
||||
Log.d(TAG, "new inline suggestion view ready");
|
||||
if(suggestionView != null) {
|
||||
suggestionView.setLayoutParams(new ViewGroup.LayoutParams(
|
||||
size.getWidth(), size.getHeight()));
|
||||
suggestionView.setOnClickListener((v) -> {
|
||||
Log.d(TAG, "Received click on the suggestion");
|
||||
});
|
||||
suggestionView.setOnLongClickListener((v) -> {
|
||||
Log.d(TAG, "Received long click on the suggestion");
|
||||
return true;
|
||||
});
|
||||
final SuggestionItem suggestionItem = new SuggestionItem(
|
||||
suggestionView, /*isAction*/ inlineSuggestion.getInfo().isPinned());
|
||||
suggestionMap.put(index, suggestionItem);
|
||||
} else {
|
||||
suggestionMap.put(index, null);
|
||||
}
|
||||
|
||||
// Update the UI once the last inflation completed
|
||||
if (suggestionMap.size() >= totalSuggestionsCount) {
|
||||
final ArrayList<SuggestionItem> suggestionItems = new ArrayList<>(
|
||||
suggestionMap.values());
|
||||
getMainExecutor().execute(() -> updateInlineSuggestionStrip(suggestionItems));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,4 +259,14 @@ public class AutofillImeService extends InputMethodService {
|
||||
Log.d(TAG, "handle() called: [" + data + "]");
|
||||
mDecoder.decodeAndApply(data);
|
||||
}
|
||||
|
||||
static class SuggestionItem {
|
||||
final InlineContentView mView;
|
||||
final boolean mIsPinned;
|
||||
|
||||
SuggestionItem(InlineContentView view, boolean isPinned) {
|
||||
mView = view;
|
||||
mIsPinned = isPinned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2020 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.example.android.autofillkeyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Choreographer;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.inline.InlineContentView;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
/**
|
||||
* This class is a container for showing {@link InlineContentView}s for cases
|
||||
* where you want to ensure they appear only in a given area in your app. An
|
||||
* example is having a scrollable list of items. Note that without this container
|
||||
* the InlineContentViews' surfaces would cover parts of your app as these surfaces
|
||||
* are owned by another process and always appearing on top of your app.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
public class InlineContentClipView extends FrameLayout {
|
||||
// The trick that we use here is to have a hidden SurfaceView to whose
|
||||
// surface we reparent the surfaces of remote content views which are
|
||||
// InlineContentViews. Since surface locations are based off the window
|
||||
// top-left making, making one surface parent of another compounds the
|
||||
// offset from the child's point of view. To compensate for that we
|
||||
// apply transformation to the InlineContentViews.
|
||||
|
||||
@NonNull
|
||||
private final ArraySet<InlineContentView> mReparentedDescendants = new ArraySet<>();
|
||||
|
||||
@NonNull
|
||||
private final int[] mTempLocation = new int[2];
|
||||
|
||||
@NonNull
|
||||
SurfaceView mSurfaceClipView;
|
||||
|
||||
private int mBackgroundColor;
|
||||
|
||||
public InlineContentClipView(@NonNull Context context) {
|
||||
this(context, /*attrs*/ null);
|
||||
}
|
||||
|
||||
public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, /*defStyleAttr*/ 0);
|
||||
}
|
||||
|
||||
public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
@AttrRes int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
mSurfaceClipView = new SurfaceView(context);
|
||||
mSurfaceClipView.setZOrderOnTop(true);
|
||||
mSurfaceClipView.getHolder().setFormat(PixelFormat.TRANSPARENT);
|
||||
mSurfaceClipView.setLayoutParams(new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
mSurfaceClipView.getHolder().addCallback(new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceCreated(@NonNull SurfaceHolder holder) {
|
||||
drawBackgroundColorIfReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
|
||||
int height) { /*do nothing*/ }
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
|
||||
updateState(InlineContentClipView.this, /*parentSurfaceProvider*/ null);
|
||||
}
|
||||
});
|
||||
|
||||
addView(mSurfaceClipView);
|
||||
|
||||
setWillNotDraw(false);
|
||||
|
||||
getViewTreeObserver().addOnPreDrawListener(() -> {
|
||||
updateState(InlineContentClipView.this, mSurfaceClipView);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundColor(int color) {
|
||||
mBackgroundColor = color;
|
||||
Choreographer.getInstance().postFrameCallback((frameTimeNanos) ->
|
||||
drawBackgroundColorIfReady());
|
||||
}
|
||||
|
||||
private void drawBackgroundColorIfReady() {
|
||||
final Surface surface = mSurfaceClipView.getHolder().getSurface();
|
||||
if (surface.isValid()) {
|
||||
final Canvas canvas = surface.lockCanvas(null);
|
||||
try {
|
||||
canvas.drawColor(mBackgroundColor);
|
||||
} finally {
|
||||
surface.unlockCanvasAndPost(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the surfaces of the {@link InlineContentView}s wrapped by this view
|
||||
* should appear on top or behind this view's window. Normally, they are placed on top
|
||||
* of the window, to allow interaction ith the embedded UI. Via this method, you can
|
||||
* place the surface below the window. This means that all of the contents of the window
|
||||
* this view is in will be visible on top of the {@link InlineContentView}s' surfaces.
|
||||
*
|
||||
* @param onTop Whether to show the surface on top of this view's window.
|
||||
*
|
||||
* @see InlineContentView
|
||||
* @see InlineContentView#setZOrderedOnTop(boolean)
|
||||
*/
|
||||
public void setZOrderedOnTop(boolean onTop) {
|
||||
mSurfaceClipView.setZOrderOnTop(onTop);
|
||||
for (InlineContentView inlineContentView : mReparentedDescendants) {
|
||||
inlineContentView.setZOrderedOnTop(onTop);
|
||||
}
|
||||
}
|
||||
|
||||
void updateState(@NonNull View root,
|
||||
@Nullable SurfaceView parentSurfaceProvider) {
|
||||
if (parentSurfaceProvider != null) {
|
||||
mSurfaceClipView.getLocationInWindow(mTempLocation);
|
||||
} else {
|
||||
mTempLocation[0] = 0;
|
||||
mTempLocation[1] = 0;
|
||||
}
|
||||
reparentChildSurfaceViewSurfacesRecursive(root, parentSurfaceProvider,
|
||||
/*parentSurfaceLeft*/ mTempLocation[0], /*parentSurfaceTop*/ mTempLocation[1]);
|
||||
}
|
||||
|
||||
private void reparentChildSurfaceViewSurfacesRecursive(@Nullable View root,
|
||||
@Nullable SurfaceView parentSurfaceProvider, int parentSurfaceLeft,
|
||||
int parentSurfaceTop) {
|
||||
if (root == null || root == mSurfaceClipView) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (root instanceof InlineContentView) {
|
||||
// Surfaces of a surface view have a transformation matrix relative
|
||||
// to the top-left of the window and when one is reparented to the
|
||||
// other the transformation adds up and we need to compensate.
|
||||
root.setTranslationX(-parentSurfaceLeft);
|
||||
root.setTranslationY(-parentSurfaceTop);
|
||||
|
||||
final InlineContentView inlineContentView = (InlineContentView) root;
|
||||
if (parentSurfaceProvider != null) {
|
||||
if (mReparentedDescendants.contains(inlineContentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
inlineContentView.setSurfaceControlCallback(
|
||||
new InlineContentView.SurfaceControlCallback() {
|
||||
@Override
|
||||
public void onCreated(SurfaceControl surfaceControl) {
|
||||
// Our surface and its descendants are initially hidden until
|
||||
// the descendants are reparented and their containers scrolled.
|
||||
new SurfaceControl.Transaction()
|
||||
.reparent(surfaceControl, parentSurfaceProvider.getSurfaceControl())
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyed(SurfaceControl surfaceControl) {
|
||||
/* do nothing */
|
||||
}
|
||||
});
|
||||
|
||||
mReparentedDescendants.add(inlineContentView);
|
||||
} else {
|
||||
if (!mReparentedDescendants.contains(inlineContentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unparent the surface control of the removed surface view.
|
||||
final SurfaceControl surfaceControl = inlineContentView.getSurfaceControl();
|
||||
if (surfaceControl != null && surfaceControl.isValid()) {
|
||||
new SurfaceControl.Transaction()
|
||||
.reparent(surfaceControl, /*newParent*/ null)
|
||||
.apply();
|
||||
}
|
||||
|
||||
mReparentedDescendants.remove(inlineContentView);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (root instanceof ViewGroup) {
|
||||
final ViewGroup rootGroup = (ViewGroup) root;
|
||||
final int childCount = rootGroup.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = rootGroup.getChildAt(i);
|
||||
reparentChildSurfaceViewSurfacesRecursive(child, parentSurfaceProvider,
|
||||
parentSurfaceLeft, parentSurfaceTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import android.service.autofill.SaveRequest;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.View;
|
||||
import android.view.autofill.AutofillId;
|
||||
import android.view.autofill.AutofillValue;
|
||||
import android.widget.inline.InlinePresentationSpec;
|
||||
@@ -78,7 +79,7 @@ public class InlineFillService extends AutofillService {
|
||||
// Find autofillable fields
|
||||
AssistStructure structure = getLatestAssistStructure(request);
|
||||
|
||||
ArrayMap<String, AutofillId> fields = getAutofillableFields(structure, request.getFlags());
|
||||
ArrayMap<String, AutofillId> fields = getAutofillableFields(structure);
|
||||
Log.d(TAG, "autofillable fields:" + fields);
|
||||
|
||||
if (fields.isEmpty()) {
|
||||
@@ -176,15 +177,19 @@ public class InlineFillService extends AutofillService {
|
||||
}
|
||||
}
|
||||
|
||||
// if (inlineRequest != null) {
|
||||
// // Reuse the first spec's height for the inline action size, as there isn't dedicated
|
||||
// // value from the request for this.
|
||||
// final int height = inlineRequest.getPresentationSpecs().get(0).getMinSize().getHeight();
|
||||
// final Size actionIconSize = new Size(height, height);
|
||||
// response.addDataset(
|
||||
// newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
|
||||
// fields));
|
||||
// }
|
||||
if (inlineRequest != null) {
|
||||
// Reuse the first spec's height for the inline action size, as there isn't dedicated
|
||||
// value from the request for this.
|
||||
final int height = inlineRequest.getInlinePresentationSpecs().get(0)
|
||||
.getMinSize().getHeight();
|
||||
final Size actionIconSize = new Size(height, height);
|
||||
response.addDataset(
|
||||
newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
|
||||
fields));
|
||||
response.addDataset(
|
||||
newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
|
||||
fields));
|
||||
}
|
||||
|
||||
// 2.Add save info
|
||||
Collection<AutofillId> ids = fields.values();
|
||||
@@ -279,57 +284,50 @@ public class InlineFillService extends AutofillService {
|
||||
* <p>An autofillable field is a {@link ViewNode} whose getHint(ViewNode) method.
|
||||
*/
|
||||
@NonNull
|
||||
private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure,
|
||||
int flags) {
|
||||
private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) {
|
||||
ArrayMap<String, AutofillId> fields = new ArrayMap<>();
|
||||
int nodes = structure.getWindowNodeCount();
|
||||
for (int i = 0; i < nodes; i++) {
|
||||
ViewNode node = structure.getWindowNodeAt(i).getRootViewNode();
|
||||
addAutofillableFields(fields, node, flags);
|
||||
addAutofillableFields(fields, node);
|
||||
}
|
||||
return fields;
|
||||
ArrayMap<String, AutofillId> result = new ArrayMap<>();
|
||||
int filedCount = fields.size();
|
||||
for (int i = 0; i < filedCount; i++) {
|
||||
String key = fields.keyAt(i);
|
||||
AutofillId value = fields.valueAt(i);
|
||||
// For fields with no hint we just use Field
|
||||
if (key.equals(value.toString())) {
|
||||
result.put("Field:" + i + "-", fields.valueAt(i));
|
||||
} else {
|
||||
result.put(key, fields.valueAt(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds any autofillable view from the {@link ViewNode} and its descendants to the map.
|
||||
*/
|
||||
private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
|
||||
@NonNull ViewNode node, int flags) {
|
||||
int type = node.getAutofillType();
|
||||
String hint = getHint(node, flags);
|
||||
if (hint != null) {
|
||||
AutofillId id = node.getAutofillId();
|
||||
if (!fields.containsKey(hint)) {
|
||||
Log.v(TAG, "Setting hint " + hint + " on " + id);
|
||||
fields.put(hint, id);
|
||||
} else {
|
||||
Log.v(TAG, "Ignoring hint " + hint + " on " + id
|
||||
+ " because it was already set");
|
||||
@NonNull ViewNode node) {
|
||||
if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) {
|
||||
if (!fields.containsValue(node.getAutofillId())) {
|
||||
final String key;
|
||||
if (node.getHint() != null) {
|
||||
key = node.getHint().toLowerCase();
|
||||
} else {
|
||||
key = node.getAutofillId().toString();
|
||||
}
|
||||
fields.put(key, node.getAutofillId());
|
||||
}
|
||||
}
|
||||
int childrenSize = node.getChildCount();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
addAutofillableFields(fields, node.getChildAt(i), flags);
|
||||
addAutofillableFields(fields, node.getChildAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the autofill hint associated with the given node.
|
||||
*
|
||||
* <p>By default it just return the first entry on the node's
|
||||
* {@link ViewNode#getAutofillHints() autofillHints} (when available), but subclasses could
|
||||
* extend it to use heuristics when the app developer didn't explicitly provide these hints.
|
||||
*/
|
||||
@Nullable
|
||||
protected String getHint(@NonNull ViewNode node, int flags) {
|
||||
String[] hints = node.getAutofillHints();
|
||||
if (hints == null) return null;
|
||||
|
||||
// We're simple, we only care about the first hint
|
||||
String hint = hints[0].toLowerCase();
|
||||
return hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the {@link AssistStructure} associated with the latest request
|
||||
* in an autofill context.
|
||||
|
||||
Reference in New Issue
Block a user