Skip to content

Commit

Permalink
Implemented a scalable control pad (remote) (#428)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
poisdeux authored and SyncedSynapse committed Aug 30, 2017
1 parent 2761c86 commit 3106a5f
Show file tree
Hide file tree
Showing 13 changed files with 1,287 additions and 591 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
328 changes: 114 additions & 214 deletions app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
240 changes: 240 additions & 0 deletions app/src/main/java/org/xbmc/kore/ui/widgets/ControlPad.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 16 additions & 0 deletions app/src/main/java/org/xbmc/kore/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,7 @@
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;
Expand Down Expand Up @@ -241,4 +243,18 @@ public void onError(int errorCode, String description) {
}
}, callbackHandler);
}

/**
* Returns the smallest width in density independent pixel size.
* Useful to determine in which sw<SIZE>dp 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);
}
}
Loading

0 comments on commit 3106a5f

Please sign in to comment.