forked from Mirroring/Kore
Implemented a scalable control pad (#477)
* 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. Refactored code to use stylesmaster
parent
bdefeafc3b
commit
b764fc676c
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import org.xbmc.kore.R;
|
||||
|
||||
/**
|
||||
* The square grid layout creates a square layout that will fit inside
|
||||
* the boundaries provided by the parent layout. Note that all cells
|
||||
* will have the same size.
|
||||
*
|
||||
* The attribute columnCount is available to specify the amount of columns
|
||||
* when using SquareGridLayout in a XML layout file.
|
||||
*/
|
||||
public class SquareGridLayout extends ViewGroup {
|
||||
|
||||
private int columnCount = 1;
|
||||
private int cellSize;
|
||||
|
||||
public SquareGridLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SquareGridLayout(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public SquareGridLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SquareGridLayout, 0, 0);
|
||||
setColumnCount(a.getInt(R.styleable.SquareGridLayout_columnCount, 1));
|
||||
a.recycle();
|
||||
fixForRelativeLayout();
|
||||
}
|
||||
|
||||
public void setColumnCount(int columnCount) {
|
||||
if (columnCount < 1) throw new IllegalArgumentException("Column count must be 1 or more");
|
||||
this.columnCount = columnCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods overridden to make sure we pass in the correct layout parameters for the child views
|
||||
*/
|
||||
@Override
|
||||
protected LayoutParams generateLayoutParams(LayoutParams p) {
|
||||
return new MarginLayoutParams(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LayoutParams generateLayoutParams(AttributeSet attrs) {
|
||||
return new MarginLayoutParams(getContext(), attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LayoutParams generateDefaultLayoutParams() {
|
||||
return new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT,
|
||||
MarginLayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
int paddingWidth = getPaddingLeft() + getPaddingRight();
|
||||
int paddingHeight = getPaddingTop() + getPaddingBottom();
|
||||
|
||||
int size;
|
||||
int padding;
|
||||
if ((width - paddingWidth) < (height - paddingHeight)) {
|
||||
size = width;
|
||||
padding = size - paddingWidth;
|
||||
} else {
|
||||
size = height;
|
||||
padding = size - paddingHeight;
|
||||
}
|
||||
|
||||
for (int y = 0; y < columnCount; y++) {
|
||||
for (int x = 0; x < columnCount; x++) {
|
||||
View child = getChildAt(y * size + x);
|
||||
if (child != null) {
|
||||
measureChildWithMargins(child,
|
||||
MeasureSpec.makeMeasureSpec((padding + x) / columnCount, MeasureSpec.EXACTLY),
|
||||
0,
|
||||
MeasureSpec.makeMeasureSpec((padding + y) / columnCount, MeasureSpec.EXACTLY),
|
||||
0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setMeasuredDimension(size, size);
|
||||
cellSize = padding;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
// top left is used to position child views
|
||||
left = getPaddingLeft();
|
||||
top = getPaddingTop();
|
||||
|
||||
for (int y = 0; y < columnCount; y++) {
|
||||
for (int x = 0; x < columnCount; x++) {
|
||||
View child = getChildAt(y * columnCount + x);
|
||||
MarginLayoutParams childLayoutParams = (MarginLayoutParams) child.getLayoutParams();
|
||||
child.layout(left + (cellSize * x) / columnCount + childLayoutParams.leftMargin,
|
||||
top + (cellSize * y) / columnCount + childLayoutParams.topMargin,
|
||||
left + (cellSize * (x+1)) / columnCount - childLayoutParams.rightMargin,
|
||||
top + (cellSize * (y+1)) / columnCount - childLayoutParams.bottomMargin
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<ImageView
|
||||
android:id="@+id/context"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="?attr/iconContext"
|
||||
android:contentDescription="@string/codec_info"/>
|
||||
<ImageView
|
||||
android:id="@+id/up"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="?attr/iconUp"
|
||||
android:contentDescription="@string/up"/>
|
||||
<ImageView
|
||||
android:id="@+id/info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="?attr/iconInfo"
|
||||
android:contentDescription="@string/info"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/left"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="?attr/iconLeft"
|
||||
android:contentDescription="@string/left"/>
|
||||
<ImageView
|
||||
android:id="@+id/select"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="?attr/iconSelect"
|
||||
android:contentDescription="@string/select"/>
|
||||
<ImageView
|
||||
android:id="@+id/right"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="?attr/iconRight"
|
||||
android:contentDescription="@string/right"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/back"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="?attr/iconBack"
|
||||
android:contentDescription="@string/back"/>
|
||||
<ImageView
|
||||
android:id="@+id/down"
|
||||
android:layout_width="0dp"
|
||||