From 3106a5fd6196022710cd983605154727df44e16e Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Wed, 30 Aug 2017 20:53:47 +0200 Subject: [PATCH] Implemented a scalable control pad (remote) (#428) * Refactored RemoteFragment and created a compound view for the actual remote. I called it ControlPad to make it more clear what its main function is. * Implemented a custom grid layout (SquareGridLayout) that will always be square. When its width and height are both set to match_parent, it will take the smallest of the two as the actual size. * For devices with a smallest width smaller then 360dp the ControlPad is sized to the maximum available space. For larger devices we still use the old fixed sizes. --- app/build.gradle | 1 + .../ui/sections/remote/RemoteFragment.java | 328 ++++++----------- .../kore/ui/viewgroups/SquareGridLayout.java | 81 +++++ .../org/xbmc/kore/ui/widgets/ControlPad.java | 240 ++++++++++++ .../main/java/org/xbmc/kore/utils/Utils.java | 16 + .../main/res/layout-land/fragment_remote.xml | 342 +++++++----------- .../layout-sw360dp-land/fragment_remote.xml | 254 +++++++++++++ .../res/layout-sw360dp/fragment_remote.xml | 244 +++++++++++++ app/src/main/res/layout/fragment_remote.xml | 262 +++++--------- .../main/res/layout/remote_control_pad.xml | 102 ++++++ app/src/main/res/layout/remote_info_panel.xml | 2 +- app/src/main/res/values-sw320dp/dimens.xml | 5 - app/src/main/res/values/dimens.xml | 1 + 13 files changed, 1287 insertions(+), 591 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/ui/viewgroups/SquareGridLayout.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/widgets/ControlPad.java create mode 100644 app/src/main/res/layout-sw360dp-land/fragment_remote.xml create mode 100644 app/src/main/res/layout-sw360dp/fragment_remote.xml create mode 100644 app/src/main/res/layout/remote_control_pad.xml delete mode 100644 app/src/main/res/values-sw320dp/dimens.xml diff --git a/app/build.gradle b/app/build.gradle index 1f2a18e..894a794 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -115,6 +115,7 @@ dependencies { compile "com.android.support:cardview-v7:${supportLibVersion}" compile "com.android.support:preference-v14:${supportLibVersion}" compile "com.android.support:support-v13:${supportLibVersion}" + compile "com.android.support:gridlayout-v7:${supportLibVersion}" compile 'com.fasterxml.jackson.core:jackson-databind:2.5.2' compile 'com.jakewharton:butterknife:6.1.0' diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java index a5f6c1d..7d23886 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java @@ -15,24 +15,14 @@ */ package org.xbmc.kore.ui.sections.remote; -import android.annotation.TargetApi; -import android.content.res.ColorStateList; -import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.BitmapFactory; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.BitmapDrawable; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -56,8 +46,8 @@ import org.xbmc.kore.jsonrpc.method.Player; import org.xbmc.kore.jsonrpc.type.GlobalType; import org.xbmc.kore.jsonrpc.type.ListType; import org.xbmc.kore.jsonrpc.type.PlayerType; +import org.xbmc.kore.ui.widgets.ControlPad; import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.RepeatListener; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; @@ -74,7 +64,8 @@ import butterknife.Optional; * Remote view */ public class RemoteFragment extends Fragment - implements HostConnectionObserver.PlayerEventsObserver { + implements HostConnectionObserver.PlayerEventsObserver, + ControlPad.OnPadButtonsListener { private static final String TAG = LogUtils.makeLogTag(RemoteFragment.class); /** @@ -102,9 +93,35 @@ public class RemoteFragment extends Fragment */ private String currentNowPlayingItemType = null; + private final Packet leftButtonPacket = + new PacketBUTTON(ButtonCodes.MAP_REMOTE, ButtonCodes.REMOTE_LEFT, false, true, + true, (short)0, (byte)0); + private final Packet rightButtonPacket = + new PacketBUTTON(ButtonCodes.MAP_REMOTE, ButtonCodes.REMOTE_RIGHT, false, true, + true, (short)0, (byte)0); + private final Packet upButtonPacket = + new PacketBUTTON(ButtonCodes.MAP_REMOTE, ButtonCodes.REMOTE_UP, false, true, + true, (short)0, (byte)0); + private final Packet downButtonPacket = + new PacketBUTTON(ButtonCodes.MAP_REMOTE, ButtonCodes.REMOTE_DOWN, false, true, + true, (short)0, (byte)0); + private final Packet selectButtonPack = + new PacketBUTTON(ButtonCodes.MAP_REMOTE, ButtonCodes.REMOTE_SELECT, false, true, + true, (short)0, (byte)0); + + private final ApiMethod downButtonAction = new Input.Down(); + private final ApiMethod leftButtonAction = new Input.Left(); + private final ApiMethod upButtonAction = new Input.Up(); + private final ApiMethod rightButtonAction = new Input.Right(); + private final ApiMethod selectButtonAction = new Input.Select(); + private final ApiMethod backButtonAction = new Input.Back(); + private final ApiMethod infoButtonAction = new Input.ExecuteAction(Input.ExecuteAction.INFO); + private final ApiMethod contextButtonAction = new Input.ExecuteAction(Input.ExecuteAction.CONTEXTMENU); + private final ApiMethod osdButtonAction = new Input.ExecuteAction(Input.ExecuteAction.OSD); + @InjectView(R.id.info_panel) RelativeLayout infoPanel; @InjectView(R.id.media_panel) RelativeLayout mediaPanel; - @InjectView(R.id.remote) RelativeLayout remotePanel; + @InjectView(R.id.remote) ControlPad controlPad; @InjectView(R.id.info_title) TextView infoTitle; @InjectView(R.id.info_message) TextView infoMessage; @@ -126,16 +143,6 @@ public class RemoteFragment extends Fragment @Optional @InjectView(R.id.weather) ImageButton weatherButton; @Optional @InjectView(R.id.system) ImageButton systemButton; - @InjectView(R.id.select) ImageView selectButton; - @InjectView(R.id.left) ImageView leftButton; - @InjectView(R.id.right) ImageView rightButton; - @InjectView(R.id.up) ImageView upButton; - @InjectView(R.id.down) ImageView downButton; - @InjectView(R.id.back) ImageView backButton; - @InjectView(R.id.info) ImageView infoButton; - @InjectView(R.id.context) ImageView contextButton; - @InjectView(R.id.osd) ImageView osdButton; - @InjectView(R.id.art) ImageView thumbnail; @InjectView(R.id.title) TextView nowPlayingTitle; @InjectView(R.id.details) TextView nowPlayingDetails; @@ -145,11 +152,6 @@ public class RemoteFragment extends Fragment @InjectView(R.id.rewind) ImageButton rewindButton; @InjectView(R.id.fast_forward) ImageButton fastForwardButton; - private Animation buttonInAnim; - private Animation buttonOutAnim; - // Touch listener that provides touch feedback - private View.OnTouchListener feedbackTouchListener; - // EventServer connection private EventServerConnection eventServerConnection = null; @@ -162,27 +164,7 @@ public class RemoteFragment extends Fragment hostManager = HostManager.getInstance(getActivity()); hostConnectionObserver = hostManager.getHostConnectionObserver(); - buttonInAnim = AnimationUtils.loadAnimation(getActivity(), R.anim.button_in); - buttonOutAnim = AnimationUtils.loadAnimation(getActivity(), R.anim.button_out); - createEventServerConnection(); - - feedbackTouchListener = new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - buttonInAnim.setFillAfter(true); - v.startAnimation(buttonInAnim); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - v.startAnimation(buttonOutAnim); - break; - } - return false; - } - }; } @Override @@ -190,43 +172,9 @@ public class RemoteFragment extends Fragment ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_remote, container, false); ButterKnife.inject(this, root); - if (hostManager.getHostInfo().getUseEventServer() && - (eventServerConnection != null)) { - // Setup d-pad to use EventServer - setupEventServerButton(leftButton, ButtonCodes.REMOTE_LEFT); - setupEventServerButton(rightButton, ButtonCodes.REMOTE_RIGHT); - setupEventServerButton(upButton, ButtonCodes.REMOTE_UP); - setupEventServerButton(downButton, ButtonCodes.REMOTE_DOWN); + controlPad.setOnPadButtonsListener(this); - setupEventServerButton(selectButton, ButtonCodes.REMOTE_SELECT); - } else { - // Otherwise, use json-rpc - setupRepeatButton(leftButton, new Input.Left()); - setupRepeatButton(rightButton, new Input.Right()); - setupRepeatButton(upButton, new Input.Up()); - setupRepeatButton(downButton, new Input.Down()); - - setupDefaultButton(selectButton, new Input.Select(), null); - } - - // Other buttons - setupDefaultButton(backButton, new Input.Back(), null); - setupDefaultButton(osdButton, new Input.ExecuteAction(Input.ExecuteAction.OSD), null); - setupDefaultButton(contextButton, new Input.ExecuteAction(Input.ExecuteAction.CONTEXTMENU), null); - - // Info button, v17 uses a different window to display codec info so check version number HostInfo hostInfo = hostManager.getHostInfo(); - if (hostInfo.getKodiVersionMajor() < 17) { - setupDefaultButton(infoButton, - new Input.ExecuteAction(Input.ExecuteAction.INFO), - new Input.ExecuteAction(Input.ExecuteAction.CODECINFO)); - } else { - setupDefaultButton(infoButton, - new Input.ExecuteAction(Input.ExecuteAction.INFO), - new Input.ExecuteAction(Input.ExecuteAction.PLAYERPROCESSINFO)); - } - - adjustRemoteButtons(); TypedArray styledAttributes = getActivity().getTheme().obtainStyledAttributes(new int[] { R.attr.iconFastForward, @@ -253,53 +201,9 @@ public class RemoteFragment extends Fragment buttons[i].setVisibility(shownItems.contains(String.valueOf(i)) ? View.VISIBLE : View.GONE); } -// // Pad main content view to account for bottom system bar -// UIUtils.setPaddingForSystemBars(getActivity(), root, false, false, true); - - // No clipping -// root.setClipToPadding(false); - return root; } - @TargetApi(21) - private void adjustRemoteButtons() { - Resources.Theme theme = getActivity().getTheme(); - TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { - R.attr.remoteButtonColorFilter, - R.attr.contentBackgroundColor}); -// R.attr.remoteBackgroundColorFilter}); - Resources resources = getResources(); - int remoteButtonsColor = styledAttributes.getColor(styledAttributes.getIndex(0), resources.getColor(R.color.white)), - remoteBackgroundColor = styledAttributes.getColor(styledAttributes.getIndex(1), resources.getColor(R.color.dark_content_background_dim_70pct)); - styledAttributes.recycle(); - - leftButton.setColorFilter(remoteButtonsColor); - rightButton.setColorFilter(remoteButtonsColor); - upButton.setColorFilter(remoteButtonsColor); - downButton.setColorFilter(remoteButtonsColor); - - selectButton.setColorFilter(remoteButtonsColor); - backButton.setColorFilter(remoteButtonsColor); - infoButton.setColorFilter(remoteButtonsColor); - osdButton.setColorFilter(remoteButtonsColor); - contextButton.setColorFilter(remoteButtonsColor); - - - // On ICS the remote background isn't shown as the tinting isn't supported - //int backgroundResourceId = R.drawable.remote_background_square_black_alpha; - int backgroundResourceId = R.drawable.remote_background_square_black; - if (Utils.isLollipopOrLater()) { - remotePanel.setBackgroundTintList(ColorStateList.valueOf(remoteBackgroundColor)); - remotePanel.setBackgroundResource(backgroundResourceId); - } else if (Utils.isJellybeanOrLater()) { - BitmapDrawable background = new BitmapDrawable(getResources(), - BitmapFactory.decodeResource(getResources(), backgroundResourceId)); - background.setColorFilter(new PorterDuffColorFilter(remoteBackgroundColor, PorterDuff.Mode.SRC_IN)); - remotePanel.setBackground(background); - } - } - @Override public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -338,91 +242,6 @@ public class RemoteFragment extends Fragment }); } - private void setupRepeatButton(View button, final ApiMethod action) { - button.setOnTouchListener(new RepeatListener(UIUtils.initialButtonRepeatInterval, UIUtils.buttonRepeatInterval, - new View.OnClickListener() { - @Override - public void onClick(View v) { - action.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); - } - }, buttonInAnim, buttonOutAnim, getActivity().getApplicationContext())); - } - - private void setupDefaultButton(View button, - final ApiMethod clickAction, - final ApiMethod longClickAction) { - // Set animation - button.setOnTouchListener(feedbackTouchListener); - if (clickAction != null) { - button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - UIUtils.handleVibration(getActivity()); - clickAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); - } - }); - } - if (longClickAction != null) { - button.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - UIUtils.handleVibration(getActivity()); - longClickAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); - return true; - } - }); - } - } - - private void setupEventServerButton(View button, final String action) { - final Packet packet = - new PacketBUTTON(ButtonCodes.MAP_REMOTE, action, false, true, true, (short)0, (byte)0); - button.setOnTouchListener(new RepeatListener(UIUtils.initialButtonRepeatInterval, UIUtils.buttonRepeatInterval, - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (eventServerConnection != null) - eventServerConnection.sendPacket(packet); - } - }, buttonInAnim, buttonOutAnim, getActivity().getApplicationContext())); - } - - -// private void setupEventServerButton(View button, final String action) { -// short amount = 0; -// byte axis = 0; -// final Packet packetDown = -// new PacketBUTTON(ButtonCodes.MAP_REMOTE, action, true, true, false, amount, axis); -// final Packet packetUp = -// new PacketBUTTON(ButtonCodes.MAP_REMOTE, action, false, false, false, amount, axis); -// -// // Set animation and packet -// button.setOnTouchListener(new View.OnTouchListener() { -// @Override -// public boolean onTouch(View v, MotionEvent event) { -// switch (event.getAction()) { -// case MotionEvent.ACTION_DOWN: -// buttonInAnim.setFillAfter(true); -// v.startAnimation(buttonInAnim); -// eventServerConnection.sendPacket(packetDown); -// break; -// case MotionEvent.ACTION_UP: -// case MotionEvent.ACTION_CANCEL: -// v.startAnimation(buttonOutAnim); -// eventServerConnection.sendPacket(packetUp); -// break; -// } -// return false; -// } -// }); -// button.setOnClickListener(new View.OnClickListener() { -// @Override -// public void onClick(View v) { -// // Nothing to do -// } -// }); -// } - /** * Default callback for methods that don't return anything */ @@ -731,11 +550,92 @@ public class RemoteFragment extends Fragment } if (showRemote) { - remotePanel.setVisibility(View.VISIBLE); + controlPad.setVisibility(View.VISIBLE); buttonBarPanel.setVisibility(View.VISIBLE); } else { - remotePanel.setVisibility(View.GONE); + controlPad.setVisibility(View.GONE); buttonBarPanel.setVisibility(View.GONE); } } + + @Override + public void leftButtonClicked() { + if (eventServerConnection != null) { + eventServerConnection.sendPacket(leftButtonPacket); + } else { + leftButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + } + + @Override + public void rightButtonClicked() { + if (eventServerConnection != null) { + eventServerConnection.sendPacket(rightButtonPacket); + } else { + rightButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + } + + @Override + public void upButtonClicked() { + if (eventServerConnection != null) { + eventServerConnection.sendPacket(upButtonPacket); + } else { + upButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + } + + @Override + public void downButtonClicked() { + if (eventServerConnection != null) { + eventServerConnection.sendPacket(downButtonPacket); + } else { + downButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + } + + @Override + public void selectButtonClicked() { + if (eventServerConnection != null) { + eventServerConnection.sendPacket(selectButtonPack); + } else { + selectButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + } + + @Override + public void backButtonClicked() { + backButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + + @Override + public void infoButtonClicked() { + infoButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + + @Override + public boolean infoButtonLongClicked() { + ApiMethod action; + HostInfo hostInfo = hostManager.getHostInfo(); + + // Info button, v17 uses a different window to display codec info so check version number + if (hostInfo.getKodiVersionMajor() < 17) { + action = new Input.ExecuteAction(Input.ExecuteAction.CODECINFO); + } else { + action = new Input.ExecuteAction(Input.ExecuteAction.PLAYERPROCESSINFO); + } + action.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + + return true; + } + + @Override + public void contextButtonClicked() { + contextButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } + + @Override + public void osdButtonClicked() { + osdButtonAction.execute(hostManager.getConnection(), defaultActionCallback, callbackHandler); + } } diff --git a/app/src/main/java/org/xbmc/kore/ui/viewgroups/SquareGridLayout.java b/app/src/main/java/org/xbmc/kore/ui/viewgroups/SquareGridLayout.java new file mode 100644 index 0000000..c2bab55 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/viewgroups/SquareGridLayout.java @@ -0,0 +1,81 @@ +/* + * Copyright 2017 Martijn Brekhof. All rights reserved. + * + * 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.xbmc.kore.ui.viewgroups; + + +import android.content.Context; +import android.support.v7.widget.GridLayout; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.widget.RelativeLayout; + +import org.xbmc.kore.utils.LogUtils; + +/** + * The square grid layout creates a square layout that will fit inside + * the boundaries provided by the parent layout. + */ +public class SquareGridLayout extends GridLayout { + + public SquareGridLayout(Context context) { + super(context); + fixForRelativeLayout(); + } + + public SquareGridLayout(Context context, AttributeSet attrs) { + super(context, attrs); + fixForRelativeLayout(); + } + + public SquareGridLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + fixForRelativeLayout(); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int width = MeasureSpec.getSize(widthSpec); + int height = MeasureSpec.getSize(heightSpec); + int size = Math.min(width, height); + + super.onMeasure(MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)); + } + + /** + * When used in a relative layout we need to set the layout parameters to + * the correct size manually. Otherwise the grid layout will be stretched. + */ + private void fixForRelativeLayout() { + getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + ViewGroup.LayoutParams pParams = getLayoutParams(); + + if (pParams instanceof RelativeLayout.LayoutParams) { + int size = Math.min(getWidth(), getHeight()); + pParams.width = size; + pParams.height = size; + setLayoutParams(pParams); + } + + getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + }); + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/widgets/ControlPad.java b/app/src/main/java/org/xbmc/kore/ui/widgets/ControlPad.java new file mode 100644 index 0000000..413dc7d --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/widgets/ControlPad.java @@ -0,0 +1,240 @@ +/* + * Copyright 2017 Martijn Brekhof. All rights reserved. + * + * 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.xbmc.kore.ui.widgets; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.BitmapFactory; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.BitmapDrawable; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; + +import org.xbmc.kore.R; +import org.xbmc.kore.ui.viewgroups.SquareGridLayout; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.RepeatListener; +import org.xbmc.kore.utils.Utils; + +import butterknife.ButterKnife; +import butterknife.InjectView; + +public class ControlPad extends SquareGridLayout + implements View.OnClickListener, View.OnLongClickListener { + private static final String TAG = LogUtils.makeLogTag(ControlPad.class); + + private static final int initialButtonRepeatInterval = 400; // ms + private static final int buttonRepeatInterval = 80; // ms + + public interface OnPadButtonsListener { + void leftButtonClicked(); + void rightButtonClicked(); + void upButtonClicked(); + void downButtonClicked(); + void selectButtonClicked(); + void backButtonClicked(); + void infoButtonClicked(); + boolean infoButtonLongClicked(); + void contextButtonClicked(); + void osdButtonClicked(); + } + + private OnPadButtonsListener onPadButtonsListener; + + @InjectView(R.id.select) ImageView selectButton; + @InjectView(R.id.left) ImageView leftButton; + @InjectView(R.id.right) ImageView rightButton; + @InjectView(R.id.up) ImageView upButton; + @InjectView(R.id.down) ImageView downButton; + @InjectView(R.id.back) ImageView backButton; + @InjectView(R.id.info) ImageView infoButton; + @InjectView(R.id.context) ImageView contextButton; + @InjectView(R.id.osd) ImageView osdButton; + + public ControlPad(Context context) { + super(context); + initializeView(context); + } + + public ControlPad(Context context, AttributeSet attrs) { + super(context, attrs); + initializeView(context); + } + + public ControlPad(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initializeView(context); + } + + @Override + public void setOnClickListener(@Nullable View.OnClickListener l) { + throw new Error("Use setOnPadButtonsListener(listener)"); + } + + @Override + public void setOnLongClickListener(@Nullable OnLongClickListener l) { + throw new Error("Use setOnPadButtonsListener(listener)"); + } + + public void setOnPadButtonsListener(OnPadButtonsListener onPadButtonsListener) { + this.onPadButtonsListener = onPadButtonsListener; + } + + private void initializeView(Context context) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.remote_control_pad, this); + ButterKnife.inject(this, this); + + applyTheme(); + setupListeners(context); + } + + @Override + public void onClick(View v) { + if (onPadButtonsListener == null) + return; + + switch (v.getId()) { + case R.id.select: + onPadButtonsListener.selectButtonClicked(); + break; + case R.id.left: + onPadButtonsListener.leftButtonClicked(); + break; + case R.id.right: + onPadButtonsListener.rightButtonClicked(); + break; + case R.id.up: + onPadButtonsListener.upButtonClicked(); + break; + case R.id.down: + onPadButtonsListener.downButtonClicked(); + break; + case R.id.back: + onPadButtonsListener.backButtonClicked(); + break; + case R.id.info: + onPadButtonsListener.infoButtonClicked(); + break; + case R.id.context: + onPadButtonsListener.contextButtonClicked(); + break; + case R.id.osd: + onPadButtonsListener.osdButtonClicked(); + break; + default: + LogUtils.LOGD(TAG, "Unknown button "+v.getId()+" clicked"); + } + } + + @Override + public boolean onLongClick(View v) { + if ((onPadButtonsListener != null) && (v.getId() == R.id.info)) { + return onPadButtonsListener.infoButtonLongClicked(); + } + + return false; + } + + @TargetApi(21) + private void applyTheme() { + Resources.Theme theme = getContext().getTheme(); + TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { + R.attr.remoteButtonColorFilter, + R.attr.contentBackgroundColor}); + Resources resources = getResources(); + int remoteButtonsColor = styledAttributes.getColor(styledAttributes.getIndex(0), resources.getColor(R.color.white)), + remoteBackgroundColor = styledAttributes.getColor(styledAttributes.getIndex(1), resources.getColor(R.color.dark_content_background_dim_70pct)); + styledAttributes.recycle(); + + leftButton.setColorFilter(remoteButtonsColor); + rightButton.setColorFilter(remoteButtonsColor); + upButton.setColorFilter(remoteButtonsColor); + downButton.setColorFilter(remoteButtonsColor); + + selectButton.setColorFilter(remoteButtonsColor); + backButton.setColorFilter(remoteButtonsColor); + infoButton.setColorFilter(remoteButtonsColor); + osdButton.setColorFilter(remoteButtonsColor); + contextButton.setColorFilter(remoteButtonsColor); + + + // On ICS the remote background isn't shown as the tinting isn't supported + //int backgroundResourceId = R.drawable.remote_background_square_black_alpha; + int backgroundResourceId = R.drawable.remote_background_square_black; + if (Utils.isLollipopOrLater()) { + setBackgroundTintList(ColorStateList.valueOf(remoteBackgroundColor)); + setBackgroundResource(backgroundResourceId); + } else if (Utils.isJellybeanOrLater()) { + BitmapDrawable background = new BitmapDrawable(getResources(), + BitmapFactory.decodeResource(getResources(), backgroundResourceId)); + background.setColorFilter(new PorterDuffColorFilter(remoteBackgroundColor, PorterDuff.Mode.SRC_IN)); + setBackground(background); + } + } + + private void setupListeners(Context context) { + final Animation buttonInAnim = AnimationUtils.loadAnimation(context, R.anim.button_in); + final Animation buttonOutAnim = AnimationUtils.loadAnimation(context, R.anim.button_out); + + RepeatListener repeatListener = new RepeatListener(initialButtonRepeatInterval, + buttonRepeatInterval, this, + buttonInAnim, buttonOutAnim, getContext()); + + OnTouchListener feedbackTouchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + buttonInAnim.setFillAfter(true); + v.startAnimation(buttonInAnim); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + v.startAnimation(buttonOutAnim); + break; + } + return false; + } + }; + + leftButton.setOnTouchListener(repeatListener); + rightButton.setOnTouchListener(repeatListener); + upButton.setOnTouchListener(repeatListener); + downButton.setOnTouchListener(repeatListener); + setupButton(selectButton, feedbackTouchListener); + setupButton(backButton, feedbackTouchListener); + setupButton(infoButton, feedbackTouchListener); + setupButton(contextButton, feedbackTouchListener); + setupButton(osdButton, feedbackTouchListener); + } + + private void setupButton(View button, OnTouchListener feedbackTouchListener) { + button.setOnTouchListener(feedbackTouchListener); + button.setOnClickListener(this); + button.setOnLongClickListener(this); + } +} diff --git a/app/src/main/java/org/xbmc/kore/utils/Utils.java b/app/src/main/java/org/xbmc/kore/utils/Utils.java index c95f6fe..8bdeb9c 100644 --- a/app/src/main/java/org/xbmc/kore/utils/Utils.java +++ b/app/src/main/java/org/xbmc/kore/utils/Utils.java @@ -15,6 +15,7 @@ */ package org.xbmc.kore.utils; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -26,6 +27,7 @@ import android.os.Build; import android.os.Handler; import android.support.v4.app.Fragment; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.widget.Toast; import org.xbmc.kore.R; @@ -241,4 +243,18 @@ public class Utils { } }, callbackHandler); } + + /** + * Returns the smallest width in density independent pixel size. + * Useful to determine in which swdp bucket the device is placed. + * @param activity + * @return the smallest width in density independent pixel size + */ + public static int getSmallestWidthDP(Activity activity) { + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + float widthDp = metrics.widthPixels / metrics.density; + float heightDp = metrics.heightPixels / metrics.density; + return (int) Math.min(widthDp, heightDp); + } } diff --git a/app/src/main/res/layout-land/fragment_remote.xml b/app/src/main/res/layout-land/fragment_remote.xml index 44a0e4e..3ff16f7 100644 --- a/app/src/main/res/layout-land/fragment_remote.xml +++ b/app/src/main/res/layout-land/fragment_remote.xml @@ -15,218 +15,19 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw360dp-land/fragment_remote.xml b/app/src/main/res/layout-sw360dp-land/fragment_remote.xml new file mode 100644 index 0000000..69e6d8e --- /dev/null +++ b/app/src/main/res/layout-sw360dp-land/fragment_remote.xml @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw360dp/fragment_remote.xml b/app/src/main/res/layout-sw360dp/fragment_remote.xml new file mode 100644 index 0000000..7e61eb5 --- /dev/null +++ b/app/src/main/res/layout-sw360dp/fragment_remote.xml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_remote.xml b/app/src/main/res/layout/fragment_remote.xml index d54991e..d5e63b6 100644 --- a/app/src/main/res/layout/fragment_remote.xml +++ b/app/src/main/res/layout/fragment_remote.xml @@ -18,93 +18,105 @@ + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> - - - + android:layout_alignParentLeft="true"> - + - - - - - - + android:layout_height="match_parent" + android:layout_marginLeft="@dimen/remote_content_hmargin" + android:layout_marginRight="@dimen/remote_content_hmargin" + android:paddingTop="@dimen/remote_page_indicator_height"> - - - + + + + + + + + + + + + + + + + + + - + android:layout_below="@id/top_panel" + android:layout_centerHorizontal="true" + app:columnCount="3"/> - - - - - - - - - - - - diff --git a/app/src/main/res/layout/remote_control_pad.xml b/app/src/main/res/layout/remote_control_pad.xml new file mode 100644 index 0000000..c250284 --- /dev/null +++ b/app/src/main/res/layout/remote_control_pad.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/remote_info_panel.xml b/app/src/main/res/layout/remote_info_panel.xml index fc0e867..c43ef76 100644 --- a/app/src/main/res/layout/remote_info_panel.xml +++ b/app/src/main/res/layout/remote_info_panel.xml @@ -20,7 +20,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/info_panel" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="@dimen/remote_poster_height" android:layout_alignParentStart="true" android:layout_alignParentLeft="true" android:paddingTop="@dimen/remote_page_indicator_height" diff --git a/app/src/main/res/values-sw320dp/dimens.xml b/app/src/main/res/values-sw320dp/dimens.xml deleted file mode 100644 index d9a03f4..0000000 --- a/app/src/main/res/values-sw320dp/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 58dp - 174dp - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index c89fe31..f5431bc 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -60,6 +60,7 @@ 98dp 140dp + 164dp 112dp 160dp