[feat] support adaptive tablet layouts on Android and iPad

This commit is contained in:
dijunkun
2026-10-05 14:10:00 +08:00
parent 9489ede03c
commit 76eddc9f02
13 changed files with 218 additions and 33 deletions
@@ -8,7 +8,8 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:supportsRtl="true" android:usesCleartextTraffic="false">
<activity android:name=".MainActivity" android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -0,0 +1,46 @@
package cn.crossdesk.mobile;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/** Reflows the existing panels, keeping text, focus and scroll state during window resizing. */
@android.annotation.SuppressLint("ViewConstructor")
final class HomeLayout extends ScrollView {
final LinearLayout panels;
private final MobileUi ui;
private int panelHeight;
HomeLayout(MobileUi ui){
super(ui.context);this.ui=ui;
setFillViewport(true);setClipChildren(false);setClipToPadding(false);
panels=new LinearLayout(ui.context){
@Override protected void onMeasure(int widthSpec,int heightSpec){
// ScrollView offers unbounded height even with an explicit LayoutParams height.
super.onMeasure(widthSpec,MeasureSpec.makeMeasureSpec(panelHeight,MeasureSpec.EXACTLY));
}
};panels.setGravity(Gravity.TOP);
panels.setPadding(ui.dp(20),ui.dp(24),ui.dp(20),ui.dp(12));
panels.setClipChildren(false);panels.setClipToPadding(false);
addView(panels,new LayoutParams(-1,-1));
}
@Override protected void onMeasure(int widthSpec,int heightSpec){
boolean wide=MeasureSpec.getSize(widthSpec)>=ui.dp(900);
panels.setOrientation(wide?LinearLayout.HORIZONTAL:LinearLayout.VERTICAL);
// A short split window or the IME can leave less room than the two panels need.
// Keep the recent list usable and let the outer viewport scroll in that case.
panelHeight=Math.max(ui.dp(wide?360:440),MeasureSpec.getSize(heightSpec));
if(panels.getChildCount()==2){
View connection=panels.getChildAt(0),recent=panels.getChildAt(1);
LinearLayout.LayoutParams first=(LinearLayout.LayoutParams)connection.getLayoutParams();
first.width=wide?ui.dp(360):-1;first.height=-2;first.weight=0;first.gravity=Gravity.TOP;
LinearLayout.LayoutParams second=(LinearLayout.LayoutParams)recent.getLayoutParams();
second.width=wide?0:-1;second.height=wide?-1:0;second.weight=1;
second.topMargin=wide?0:ui.dp(20);second.setMarginStart(wide?ui.dp(24):0);
second.resolveLayoutDirection(panels.getLayoutDirection());
}
super.onMeasure(widthSpec,heightSpec);
}
}
@@ -46,6 +46,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private RecentConnections history;
private final RecentConnectionPresence recentPresence=new RecentConnectionPresence();
private LinearLayout recentGrid;
private int recentColumns=2;
private final Runnable presenceTick=new Runnable(){public void run(){
if(!recentPresence.isConnected())return;
recentPresence.maintain(android.os.SystemClock.elapsedRealtime());
@@ -114,8 +115,10 @@ public final class MainActivity extends Activity implements NativeSession.Listen
settingsUpdateDot=null;aboutUpdateDot=null;settingsUpdateButton=null;aboutUpdateRow=null;aboutUpdateLabel=null;
previewSwitch=null;clearPreviewsButton=null;previewCleanupError=null;previewCards.clear();recentGrid=null;
canvas=new FrameLayout(this);canvas.setBackgroundColor(dark?Color.BLACK:BACKGROUND);
root=column();canvas.addView(root,new FrameLayout.LayoutParams(-1,-1));
root.setOnApplyWindowInsetsListener((view,insets)->{
FrameLayout safeArea=new FrameLayout(this);canvas.addView(safeArea,new FrameLayout.LayoutParams(-1,-1));
root=ui.pageColumn(dark?0:nextPage.equals("home")?1200:720);
safeArea.addView(root,new FrameLayout.LayoutParams(-1,-1,Gravity.TOP|Gravity.CENTER_HORIZONTAL));
safeArea.setOnApplyWindowInsetsListener((view,insets)->{
view.setPadding(insets.getSystemWindowInsetLeft(),insets.getSystemWindowInsetTop(),insets.getSystemWindowInsetRight(),insets.getSystemWindowInsetBottom());return insets;
});
root.setFocusableInTouchMode(true);pageHost.show(canvas,motion);
@@ -155,9 +158,18 @@ public final class MainActivity extends Activity implements NativeSession.Listen
TextView chevron=text(" ›",23,false);chevron.setTextColor(0xFFB7B7BD);item.addView(chevron);item.setContentDescription(title+(detail.isEmpty()?"":","+detail));item.setClickable(true);item.setFocusable(true);item.setOnClickListener(v->action.run());parent.addView(item);
return item;
}
@android.annotation.SuppressLint("SourceLockedOrientationActivity") // Match the iOS portrait home; sessions support both orientations.
@android.annotation.SuppressLint("SourceLockedOrientationActivity") // Phones keep the portrait home; tablets and multi-window follow user rotation settings.
private void updateHomeOrientation(){
boolean tablet=getResources().getConfiguration().smallestScreenWidthDp>=600;
if(android.os.Build.VERSION.SDK_INT>=30){
android.graphics.Rect bounds=getWindowManager().getMaximumWindowMetrics().getBounds();
tablet|=Math.min(bounds.width(),bounds.height())>=dp(600);
}
int orientation=tablet||isInMultiWindowMode()?ActivityInfo.SCREEN_ORIENTATION_FULL_USER:ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
if(getRequestedOrientation()!=orientation)setRequestedOrientation(orientation);
}
private void home(String message){
backAction=null;setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);installRoot("home",false);
backAction=null;updateHomeOrientation();installRoot("home",false);
LinearLayout toolbar=row();toolbar.setPadding(dp(20),0,dp(20),0);root.addView(toolbar,ui.size(-1,56));
signalBadge=text("",12,true);signalBadge.setGravity(Gravity.CENTER);signalBadge.setPadding(dp(11),0,dp(11),0);signalBadge.setSingleLine(true);
signalBadge.setOnClickListener(v->{
@@ -173,7 +185,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
TextView updateBadge=text("!",10,true);updateBadge.setTextColor(Color.WHITE);updateBadge.setGravity(Gravity.CENTER);updateBadge.setIncludeFontPadding(false);updateBadge.setBackground(ui.background(RED,8));updateBadge.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);settingsUpdateDot=updateBadge;
FrameLayout.LayoutParams updatePosition=new FrameLayout.LayoutParams(dp(16),dp(16),Gravity.TOP|Gravity.END);updatePosition.topMargin=-dp(3);updatePosition.setMarginEnd(-dp(5));settingsButton.addView(settingsUpdateDot,updatePosition);
toolbar.addView(settingsButton,ui.size(40,40));renderAppUpdates();
LinearLayout content=column();content.setPadding(dp(20),dp(24),dp(20),dp(12));content.setClipChildren(false);content.setClipToPadding(false);root.addView(content,new LinearLayout.LayoutParams(-1,0,1));
HomeLayout layout=new HomeLayout(ui);root.addView(layout,new LinearLayout.LayoutParams(-1,0,1));LinearLayout content=layout.panels;
LinearLayout connection=column();connection.setPadding(dp(18),dp(18),dp(18),dp(18));content.addView(ui.shadowCard(connection,20));
connection.addView(text("远程桌面",20,true));ui.gap(connection,16);
LinearLayout entry=row();connection.addView(entry,ui.size(-1,54));
@@ -191,7 +203,12 @@ public final class MainActivity extends Activity implements NativeSession.Listen
LinearLayout empty=column();empty.setGravity(Gravity.CENTER);recent.addView(empty,new LinearLayout.LayoutParams(-1,0,1));empty.addView(ui.icon("history",0xFFB6B6BB),ui.size(34,34));ui.gap(empty,12);
TextView emptyTitle=text("还没有连接记录",17,true);emptyTitle.setGravity(Gravity.CENTER);empty.addView(emptyTitle);ui.gap(empty,8);TextView hint=text("成功连接后,这里会显示最近的远程设备。",13,false);hint.setTextColor(SECONDARY);hint.setGravity(Gravity.CENTER);empty.addView(hint);
}else{
ui.gap(recent,14);ScrollView scroll=new ScrollView(this);recent.addView(scroll,new LinearLayout.LayoutParams(-1,0,1));recentGrid=column();scroll.addView(recentGrid);renderRecentConnections();
ui.gap(recent,14);ScrollView scroll=new ScrollView(this);recent.addView(scroll,new LinearLayout.LayoutParams(-1,0,1));recentGrid=column();scroll.addView(recentGrid);
recentGrid.addOnLayoutChangeListener((view,left,top,right,bottom,oldLeft,oldTop,oldRight,oldBottom)->{
int width=right-left;if(width<=0)return;
int columns=width<dp(300)?1:Math.max(2,Math.min(4,(width+dp(12))/dp(192)));
if(columns!=recentColumns){recentColumns=columns;view.post(()->{if(view==recentGrid)renderRecentConnections();});}
});renderRecentConnections();
}
ensureSignaling();
if(!message.isEmpty())new AlertDialog.Builder(this).setTitle("连接提示").setMessage(message).setPositiveButton("确定",null).show();
@@ -202,7 +219,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
if(recentGrid==null)return;
JSONArray records=recentPresence.ordered(history.list(serverKey()),android.os.SystemClock.elapsedRealtime());
recentGrid.removeAllViews();previewCards.clear();
for(int i=0;i<records.length();i+=2){LinearLayout line=row();line.setGravity(Gravity.TOP);recentGrid.addView(line);for(int j=i;j<i+2;j++){View card=j<records.length()?recentCard(records.optJSONObject(j)):new View(this);LinearLayout.LayoutParams cell=new LinearLayout.LayoutParams(0,-2,1);if(j>i)cell.leftMargin=dp(12);line.addView(card,cell);}ui.gap(recentGrid,12);}
for(int i=0;i<records.length();i+=recentColumns){LinearLayout line=row();line.setGravity(Gravity.TOP);recentGrid.addView(line);for(int j=i;j<i+recentColumns;j++){View card=j<records.length()?recentCard(records.optJSONObject(j)):new View(this);LinearLayout.LayoutParams cell=new LinearLayout.LayoutParams(0,-2,1);if(j>i)cell.setMarginStart(dp(12));line.addView(card,cell);}ui.gap(recentGrid,12);}
}
private View recentCard(JSONObject item){
if(item==null)return new View(this);String id=item.optString("id"),platform=item.optString("platform");
@@ -249,9 +266,16 @@ public final class MainActivity extends Activity implements NativeSession.Listen
String secret=password.getText().toString();if(secret.isEmpty()||secret.length()>128||secret.contains("@")){password.setError("请输入有效访问密码");return;}
rememberPassword=remember.isChecked();password.setText("");dialog.dismiss();begin(id,secret);
});connect.setContentDescription("确认连接");sheet.addView(connect,ui.size(-1,48));
dialog.setContentView(sheet);Window window=dialog.getWindow();if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);window.setDimAmount(.22f);}dialog.show();if(window!=null){window.setLayout(-1,-2);window.setGravity(Gravity.BOTTOM);}password.requestFocus();if(!error.isEmpty())password.setError(error);
ScrollView scroll=new ScrollView(this);scroll.addView(sheet);
dialog.setContentView(scroll);Window window=dialog.getWindow();if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);window.setDimAmount(.22f);}dialog.show();sizePasswordDialog();password.requestFocus();if(!error.isEmpty())password.setError(error);
dialog.setOnDismissListener(d->{password.setText("");passwordDialog=null;});
}
private void sizePasswordDialog(){
if(passwordDialog==null||passwordDialog.getWindow()==null)return;
int width=getResources().getConfiguration().screenWidthDp;
Window window=passwordDialog.getWindow();window.setLayout(width>=600?dp(Math.min(520,width-48)):-1,-2);
window.setGravity(width>=600?Gravity.CENTER:Gravity.BOTTOM);
}
private void begin(String id,String secret){
resetRemoteVersionCheck();
resetVideoSettings();
@@ -538,7 +562,14 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private void back(){pageHost.finish();if(session!=null){if(ready)confirmDisconnect();else end("");}else if(backAction!=null)navigateBack();else finish();}
@Override public void onWindowFocusChanged(boolean focus){super.onWindowFocusChanged(focus);if(!focus)releaseKeys();else applySystemBars(ready);}
@android.annotation.SuppressLint("GestureBackNavigation") @Override public void onBackPressed(){back();}
@Override public void onConfigurationChanged(Configuration config){super.onConfigurationChanged(config);if(controls!=null)controls.post(controls::constrainPanels);}
@Override public void onConfigurationChanged(Configuration config){
super.onConfigurationChanged(config);
releaseKeys();
if(!page.equals("session"))updateHomeOrientation();
sizePasswordDialog();
if(privacyDialog instanceof PrivacyConsentDialog)((PrivacyConsentDialog)privacyDialog).updateSize();
if(controls!=null)controls.post(controls::constrainPanels);
}
// onStart marks a new visit; focus/resume callbacks must not reopen a refused notice.
@Override protected void onStart(){super.onStart();foreground=true;appUpdates.setEnabled(preferences.getBoolean("networkConsent",false));if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling);ensureSignaling();}
@Override protected void onStop(){foreground=false;appUpdates.setEnabled(false);if(privacyDialog!=null)privacyDialog.dismiss();pageHost.finish();if(session!=null)end(isFinishing()?"":"应用已进入后台,会话已结束");stopSignaling();super.onStop();}
@@ -29,6 +29,16 @@ final class MobileUi {
return wrapper;
}
LinearLayout column() { LinearLayout v=new LinearLayout(context);v.setOrientation(LinearLayout.VERTICAL);return v; }
LinearLayout pageColumn(int maxWidth) {
LinearLayout v=new LinearLayout(context){
@Override protected void onMeasure(int widthSpec,int heightSpec){
if(maxWidth>0&&MeasureSpec.getSize(widthSpec)>dp(maxWidth))
widthSpec=MeasureSpec.makeMeasureSpec(dp(maxWidth),MeasureSpec.EXACTLY);
super.onMeasure(widthSpec,heightSpec);
}
};
v.setOrientation(LinearLayout.VERTICAL);return v;
}
LinearLayout row() { LinearLayout v=new LinearLayout(context);v.setGravity(Gravity.CENTER_VERTICAL);return v; }
TextView text(String title,int size,boolean bold) {
TextView v=new TextView(context);v.setText(title);v.setTextSize(size);v.setTextColor(INK);v.setIncludeFontPadding(false);
@@ -12,11 +12,13 @@ import static cn.crossdesk.mobile.MobileUi.*;
/** Explicit agreement is required; refusal dismisses only the current visit. */
final class PrivacyConsentDialog extends Dialog {
private final Activity activity;
private final MobileUi ui;
PrivacyConsentDialog(Activity activity,MobileUi ui,Runnable accepted){
this(activity,ui,new PrivacyPolicyDocument(activity),accepted);
}
PrivacyConsentDialog(Activity activity,MobileUi ui,PrivacyPolicyDocument policy,Runnable accepted){
super(activity);requestWindowFeature(Window.FEATURE_NO_TITLE);setCancelable(false);setCanceledOnTouchOutside(false);
super(activity);this.activity=activity;this.ui=ui;requestWindowFeature(Window.FEATURE_NO_TITLE);setCancelable(false);setCanceledOnTouchOutside(false);
LinearLayout sheet=ui.column();sheet.setBackground(ui.background(android.graphics.Color.WHITE,20));sheet.setClipToOutline(true);
TextView title=ui.text("隐私政策",17,true);title.setGravity(Gravity.CENTER);sheet.addView(title,ui.size(-1,56));ui.divider(sheet);
sheet.addView(policy.view(ui),new LinearLayout.LayoutParams(-1,0,1));ui.divider(sheet);
@@ -31,6 +33,15 @@ final class PrivacyConsentDialog extends Dialog {
setContentView(sheet);Window window=getWindow();if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setDimAmount(.22f);}
}
@Override public void show(){
super.show();Window window=getWindow();if(window!=null){window.setLayout(-1,Math.round(getContext().getResources().getDisplayMetrics().heightPixels*.9f));window.setGravity(Gravity.BOTTOM);}
super.show();updateSize();
}
void updateSize(){
android.content.res.Configuration configuration=activity.getResources().getConfiguration();
boolean wide=configuration.screenWidthDp>=600;
Window window=getWindow();if(window!=null){
window.setLayout(wide?ui.dp(Math.min(640,configuration.screenWidthDp-48)):-1,
Math.min(ui.dp(760),Math.round(ui.dp(configuration.screenHeightDp)*.9f)));
window.setGravity(wide?Gravity.CENTER:Gravity.BOTTOM);
}
}
}
@@ -20,6 +20,8 @@ final class SessionControls extends FrameLayout {
private final LinearLayout waiting;
private View shield;
private LinearLayout menu,keyboard;
private ScrollView keyboardKeys;
private int keyboardPreferredHeight;
private TextView networkLabel,videoFeedback;
private String menuPage="controls";
private RemoteNetworkStatistics.Snapshot network=new RemoteNetworkStatistics().snapshot(0);
@@ -136,14 +138,21 @@ final class SessionControls extends FrameLayout {
private int panelWidth(int width){return Math.max(1,Math.min(width-ui.dp(16),Math.max(ui.dp(280),Math.min(ui.dp(340),width-ui.dp(160)))));}
private int panelHeight(int height){return Math.min(ui.dp(310),Math.max(1,height-ui.dp(16)));}
private void positionMenu(){if(menu==null)return;float ox=orb.getX()+orb.getWidth()/2f;float x=ox>getWidth()/2f?orb.getX()-ui.dp(12)-menu.getWidth():orb.getX()+orb.getWidth()+ui.dp(12);menu.setX(bound(x,ui.dp(8),getWidth()-menu.getWidth()-ui.dp(8)));menu.setY(bound(orb.getY(),ui.dp(8),getHeight()-menu.getHeight()-ui.dp(8)));}
private void closeKeyboard(){if(keyboard!=null){removeView(keyboard);keyboard=null;}}
private int keyboardWidth(int width){return Math.min(ui.dp(computerKeyboard?520:440),Math.max(1,width-ui.dp(16)));}
private int keyboardHeight(int height){return Math.min(keyboardPreferredHeight,Math.max(1,height-ui.dp(16)));}
private void positionKeyboard(){if(keyboard!=null){keyboard.setX(bound(keyboard.getX(),ui.dp(8),getWidth()-keyboard.getWidth()-ui.dp(8)));keyboard.setY(bound(keyboard.getY(),ui.dp(8),getHeight()-keyboard.getHeight()-ui.dp(8)));}}
private void closeKeyboard(){if(keyboard!=null){removeView(keyboard);keyboard=null;keyboardKeys=null;}}
private void toggleKeyboard(){if(keyboard!=null){closeKeyboard();modifiers.clear();return;}modifiers.clear();showKeyboard();}
private void showKeyboard(){
float oldX=keyboard==null?-1:keyboard.getX(),oldY=keyboard==null?-1:keyboard.getY();closeKeyboard();keyboard=ui.column();keyboard.setPadding(ui.dp(5),ui.dp(5),ui.dp(5),ui.dp(5));keyboard.setBackground(ui.background(0xFFD1D6DE,12));keyboard.setElevation(ui.dp(10));
float oldX=keyboard==null?-1:keyboard.getX(),oldY=keyboard==null?-1:keyboard.getY();int oldScroll=keyboardKeys==null?0:keyboardKeys.getScrollY();closeKeyboard();keyboard=ui.column();keyboard.setPadding(ui.dp(5),ui.dp(5),ui.dp(5),ui.dp(5));keyboard.setBackground(ui.background(0xFFD1D6DE,12));keyboard.setElevation(ui.dp(10));
LinearLayout top=ui.row();TextView handle=ui.text("━",19,true);handle.setGravity(Gravity.CENTER);handle.setTextColor(SECONDARY);handle.setContentDescription("拖动键盘");top.addView(handle,new LinearLayout.LayoutParams(0,ui.dp(28),1));top.addView(ui.iconButton("close","收起键盘",this::closeKeyboard),ui.size(30,28));keyboard.addView(top);drag(handle,keyboard,()->{});
// Keep the drag/close row fixed while short split windows can scroll every key into view.
keyboardKeys=new ScrollView(getContext());keyboard.addView(keyboardKeys,new LinearLayout.LayoutParams(-1,0,1));LinearLayout keys=ui.column();keyboardKeys.addView(keys);
String[][] rows=computerKeyboard?new String[][]{{"Esc","F1","F2","F3","F4","F5","F6"},{"Tab","Ctrl","Alt","Win","↑","⌫"},{"复制","粘贴","全选","←","↓","→"},{"ABC","空格","Enter"}}:new String[][]{{"1","2","3","4","5","6","7","8","9","0"},{"q","w","e","r","t","y","u","i","o","p"},{"a","s","d","f","g","h","j","k","l"},{"⇧","z","x","c","v","b","n","m","⌫"},{"电脑","剪贴板","空格",".","Enter"}};
for(String[] values:rows){LinearLayout line=ui.row();for(String value:values){String shown=shift&&value.length()==1?value.toUpperCase(java.util.Locale.ROOT):value;TextView key=ui.action(shown,INK,()->keyboardKey(value));key.setTextSize(value.length()>2?11:14);key.setMinHeight(ui.dp(31));int modifier=value.equals("Ctrl")?17:value.equals("Alt")?18:value.equals("Win")?91:0;key.setBackground(ui.background(modifiers.contains(modifier)?0xFF9BC7F7:value.length()>1||value.equals("⇧")?0xFFAFB8C5:Color.WHITE,5));LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(31),value.equals("空格")?3:1);p.setMargins(ui.dp(1.5f),0,ui.dp(1.5f),0);line.addView(key,p);}keyboard.addView(line);ui.gap(keyboard,3);}
int width=Math.min(ui.dp(computerKeyboard?520:440),Math.max(1,getWidth()-ui.dp(16)));addView(keyboard,new LayoutParams(width,-2));keyboard.post(()->{if(keyboard!=null){keyboard.setX(oldX<0?(getWidth()-keyboard.getWidth())/2f:oldX);keyboard.setY(oldY<0?getHeight()-keyboard.getHeight()-ui.dp(20):oldY);constrainPanels();}});
for(String[] values:rows){LinearLayout line=ui.row();for(String value:values){String shown=shift&&value.length()==1?value.toUpperCase(java.util.Locale.ROOT):value;TextView key=ui.action(shown,INK,()->keyboardKey(value));key.setTextSize(value.length()>2?11:14);key.setMinHeight(ui.dp(31));int modifier=value.equals("Ctrl")?17:value.equals("Alt")?18:value.equals("Win")?91:0;key.setBackground(ui.background(modifiers.contains(modifier)?0xFF9BC7F7:value.length()>1||value.equals("⇧")?0xFFAFB8C5:Color.WHITE,5));LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(31),value.equals("空格")?3:1);p.setMargins(ui.dp(1.5f),0,ui.dp(1.5f),0);line.addView(key,p);}keys.addView(line);ui.gap(keys,3);}
keyboardPreferredHeight=ui.dp(5)*2+ui.dp(28)+rows.length*(ui.dp(31)+ui.dp(3));
addView(keyboard,new LayoutParams(keyboardWidth(getWidth()),keyboardHeight(getHeight())));LinearLayout shownKeyboard=keyboard;ScrollView shownKeys=keyboardKeys;
keyboard.post(()->{if(keyboard==shownKeyboard){keyboard.setX(oldX<0?(getWidth()-keyboard.getWidth())/2f:oldX);keyboard.setY(oldY<0?getHeight()-keyboard.getHeight()-ui.dp(20):oldY);shownKeys.scrollTo(0,oldScroll);constrainPanels();}});
}
private void keyboardKey(String value){
switch(value){
@@ -177,16 +186,22 @@ final class SessionControls extends FrameLayout {
}
void constrainPanels(){
orb.setX(bound(orb.getX(),ui.dp(8),getWidth()-orb.getWidth()-ui.dp(8)));orb.setY(bound(orb.getY(),ui.dp(8),getHeight()-orb.getHeight()-ui.dp(8)));
if(menu!=null)requestLayout();
if(keyboard!=null){ViewGroup.LayoutParams p=keyboard.getLayoutParams();p.width=Math.min(ui.dp(computerKeyboard?520:440),Math.max(1,getWidth()-ui.dp(16)));keyboard.setLayoutParams(p);keyboard.setX(bound(keyboard.getX(),ui.dp(8),getWidth()-p.width-ui.dp(8)));keyboard.setY(bound(keyboard.getY(),ui.dp(8),getHeight()-keyboard.getHeight()-ui.dp(8)));}
if(menu!=null||keyboard!=null)requestLayout();
positionKeyboard();
}
@Override protected void onMeasure(int widthSpec,int heightSpec){
super.onMeasure(widthSpec,heightSpec);
boolean resized=false;
if(menu!=null){
// Use this traversal's viewport, including rotation/inset changes, before drawing.
ViewGroup.LayoutParams p=menu.getLayoutParams();int width=panelWidth(getMeasuredWidth()),height=panelHeight(getMeasuredHeight());
if(p.width!=width||p.height!=height){p.width=width;p.height=height;super.onMeasure(widthSpec,heightSpec);}
if(p.width!=width||p.height!=height){p.width=width;p.height=height;resized=true;}
}
if(keyboard!=null){
ViewGroup.LayoutParams p=keyboard.getLayoutParams();int width=keyboardWidth(getMeasuredWidth()),height=keyboardHeight(getMeasuredHeight());
if(p.width!=width||p.height!=height){p.width=width;p.height=height;resized=true;}
}
if(resized)super.onMeasure(widthSpec,heightSpec);
}
@Override protected void onLayout(boolean changed,int left,int top,int right,int bottom){
super.onLayout(changed,left,top,right,bottom);
@@ -194,6 +209,7 @@ final class SessionControls extends FrameLayout {
orb.setX(bound(orb.getX(),ui.dp(8),getWidth()-orb.getWidth()-ui.dp(8)));orb.setY(bound(orb.getY(),ui.dp(8),getHeight()-orb.getHeight()-ui.dp(8)));
// New/rebuilt menus have real dimensions now; position them in the same frame.
positionMenu();
positionKeyboard();
}
@Override protected void onSizeChanged(int width,int height,int oldWidth,int oldHeight){super.onSizeChanged(width,height,oldWidth,oldHeight);post(this::constrainPanels);}
private static float bound(float value,float min,float max){return Math.max(min,Math.min(value,Math.max(min,max)));}